The 3.0 release paired agent-client-protocol 3.x with
agent-client-protocol-rmcp 4.x and rmcp 3.x. Applications using the
integration must upgrade both public dependencies together; see the
rmcp integration migration for service and tool API
changes. The minimum supported Rust version remains 1.88.
For the other transport changes in core 3.x, follow:
- Migrating Connection Drivers for
(Channel, Option<ConnectionDriver>)and active/passive transport ownership. - Migrating the Native MCP Binding for the replacement of connect/disconnect with request-scoped MCP 2026-07-28.
- Migrating Custom HTTP Client Construction for WebSocket configuration through the transport builder instead of a prebuilt reqwest client.
- Native MCP-over-ACP for request-scoped provider usage and the MCP HTTP bridge for modern-only HTTP re-export.
For Cargo feature selection and defaults, see Cargo Features.
For non-exhaustive server options and CORS policies, see Migrating the Config API. Agent subprocess config constructors and fluent methods remain unchanged.
RawJsonRpcMessage::Response now contains RawJsonRpcResponse, whose error
variant carries Box<RawJsonRpcError> instead of an ACP Error. Typed ACP
request APIs still return ACP Error; raw transports, interceptors, and
relays must use the protocol-neutral type.
Raw error codes are i32, not ErrorCode. Raw errors distinguish omitted
data from explicit JSON null and preserve unknown error-object fields in
extra. Do not interpret MCP or other protocols' error codes as ACP codes.
Use RawJsonRpcResponse::new with a Result<Value, Box<RawJsonRpcError>>, or
construct its Result and Error variants directly. Use the constructor for
the non-exhaustive raw error struct, then set data and extensions:
use agent_client_protocol::{RawJsonRpcError, RawJsonRpcMessage, RawJsonRpcResponse};
use serde_json::{Value, json};
fn main() {
let success = RawJsonRpcResponse::new(1_i64, Ok(json!({"answer": 42})));
assert!(matches!(success, RawJsonRpcResponse::Result { .. }));
let mut error = RawJsonRpcError::new(-32000, "Tool unavailable").data(Value::Null);
error.extra.insert("retryable".into(), json!(true));
let failure = RawJsonRpcResponse::Error {
id: 2_i64.into(),
error: Box::new(error),
};
let message = RawJsonRpcMessage::Response(failure);
match message {
RawJsonRpcMessage::Response(RawJsonRpcResponse::Result { id, result }) => {
println!("{id}: {result}");
}
RawJsonRpcMessage::Response(RawJsonRpcResponse::Error { id, error }) => {
assert_eq!(error.code, -32000);
assert!(error.data.is_null());
assert_eq!(error.extra["retryable"], json!(true));
println!("{id}: {}", error.message);
}
_ => panic!("expected a response"),
}
}RawJsonRpcError::new leaves data omitted; .data(Value::Null) explicitly
includes "data": null. The boxing is required by the response type.
When a response is known to be ACP, explicitly unbox and consume the raw
error with into_acp_error. This interprets ACP error codes, keeps data
presence, and intentionally discards error extension fields:
use agent_client_protocol::{Error, ErrorCode, RawJsonRpcError, RawJsonRpcResponse};
use serde_json::{Value, json};
fn into_acp_result(response: RawJsonRpcResponse) -> Result<Value, Error> {
match response {
RawJsonRpcResponse::Result { result, .. } => Ok(result),
RawJsonRpcResponse::Error { error, .. } => Err((*error).into_acp_error()),
}
}
fn main() {
// This response belongs to ACP, where -32000 means authentication required.
let mut raw = RawJsonRpcError::new(-32000, "Authentication required").data(Value::Null);
raw.extra.insert("peerExtension".into(), json!(true));
let response = RawJsonRpcResponse::new(3_i64, Err(Box::new(raw)));
let error = into_acp_result(response).unwrap_err();
assert_eq!(error.code, ErrorCode::AuthRequired);
assert_eq!(error.data, Some(Value::Null));
let raw_again = RawJsonRpcError::from(error);
assert!(raw_again.extra.is_empty()); // Conversion is not a lossless relay.
}For locally generated ACP responses, RawJsonRpcMessage::response(id, Result<Value, Error>) remains available and converts the ACP error to its raw
representation. It is not a constructor for another protocol's errors.
Forward a raw message unchanged when no ID rewrite is needed. If a relay
maps request IDs, move the original boxed raw error into the new response;
do not round-trip it through Error or RawJsonRpcMessage::response:
use agent_client_protocol::{RawJsonRpcMessage, RawJsonRpcResponse};
use serde_json::{Value, json};
fn main() {
// A downstream protocol's -32000 is not necessarily ACP AuthRequired.
let wire = json!({
"jsonrpc": "2.0",
"id": "downstream",
"error": {
"code": -32000,
"message": "Tool unavailable",
"data": null,
"retryable": true
}
});
let message: RawJsonRpcMessage = serde_json::from_value(wire.clone()).unwrap();
let RawJsonRpcMessage::Response(response) = message else {
panic!("expected a response");
};
let upstream_id = String::from("upstream").into();
let relayed = match response {
RawJsonRpcResponse::Result { result, .. } => {
RawJsonRpcResponse::Result { id: upstream_id, result }
}
RawJsonRpcResponse::Error { error, .. } => {
RawJsonRpcResponse::Error { id: upstream_id, error }
}
};
let actual: Value = serde_json::to_value(RawJsonRpcMessage::Response(relayed)).unwrap();
let mut expected = wire;
expected["id"] = json!("upstream");
assert_eq!(actual, expected); // Code, explicit null, and extensions survive.
}The same relay rule preserves omission when the input has no data field.
See the raw response reference for
the protocol boundary.