Skip to content
Merged
2 changes: 1 addition & 1 deletion md/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ serialization and deserialization. Within `mcp_server`, these typed APIs require
`schemars`:

- `McpTool`
- `McpToolRegistry`, `RegisteredMcpTool`, and `EnabledTools`
- `McpToolRegistry`, `RegisteredMcpTool`, and `UnknownToolError`
- `McpToolMetadata` and `McpToolSchema`
- The `tool_fn` and `tool_fn_mut` functions

Expand Down
6 changes: 5 additions & 1 deletion md/http-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,12 @@ agent-client-protocol-http = { version = "3", features = ["client"] }
# agent-client-protocol-http = { version = "3", features = ["client", "server"] }
```

The `client` feature exposes `HttpClient`. The `server` feature exposes
The `client` feature exposes `HttpClient`. Its `configure_http` and
`from_http_client` hooks take reqwest types, so add a matching `reqwest`
dependency to customize the HTTP client. The `server` feature exposes
`AcpHttpServer`, `ServerOptions`, and `CorsOptions`.
`AcpHttpServer::into_router` returns an `axum::Router` to mount in your axum
application, so your axum dependency must match this crate's.

## Request Cancellation

Expand Down
2 changes: 1 addition & 1 deletion md/mcp-over-acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ roll back arbitrary detached application work.
```sh
cargo run -p agent-client-protocol-rmcp \
--example stateless_native_mcp \
--features rmcp_3,unstable_mcp_over_acp,unstable_protocol_v2
--features rmcp_3,unstable_mcp_over_acp
```

See the [wire reference](./protocol.md#native-mcp-over-acp),
Expand Down
14 changes: 8 additions & 6 deletions md/migration-config-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@ value and preserve the other options.

## CORS policies

Existing variants and constructors remain available. **Breaking:** exhaustive
matches on `CorsOptions` outside the HTTP crate must add a wildcard arm:
In 3.x, `CorsOptions` was a `#[non_exhaustive]` enum, so exhaustive matches
outside the HTTP crate needed a wildcard arm.

**Breaking:** in v5, `CorsOptions` is an opaque struct and its variants are no
longer public. Code that matched on the variants no longer compiles.
Build policies with `CorsOptions::disabled()`, `CorsOptions::allow_origins(...)`,
or `CorsOptions::allow_any_origin()`, and compare them with `==`:

```rust
use agent_client_protocol_http::CorsOptions;

fn is_disabled(policy: &CorsOptions) -> bool {
match policy {
CorsOptions::Disabled => true,
_ => false,
}
*policy == CorsOptions::disabled()
}
```

Expand Down
85 changes: 81 additions & 4 deletions md/migration-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,18 @@ fn respond<P: agent_client_protocol::Protocol>(responder: Responder<MyResponse,
`send_proxied_message` / `send_proxied_message_to` accept a `Dispatch` of any
marker, so forwarding between connections needs no conversion.

### `MatchDispatch::otherwise_ignore` answers unmatched requests

`MatchDispatch::otherwise_ignore` now answers an unmatched request with a
method not found error (`-32601`). Before, it dropped the responder, so the
peer got the dropped-responder error instead. Unmatched notifications and
responses are still ignored.

`v1::ActiveSession::read_to_string` uses `otherwise_ignore`, so a request routed
to the session that it does not handle, such as `session/request_permission`,
now gets `-32601`. Handle such requests yourself if the peer needs a different
answer.

### Renamed builder context types

The hidden `ConnectionContext` trait and its `RawConnectionContext` and
Expand Down Expand Up @@ -780,8 +792,10 @@ agent-client-protocol-rmcp = { version = "5", features = ["rmcp_3", "unstable_mc
rmcp = "3"
```

The `unstable_protocol_v2` and `unstable_mcp_over_acp` passthrough features are
unchanged and apply to every rmcp module.
The `unstable_mcp_over_acp` passthrough feature is unchanged and applies to
every rmcp module. The `unstable_protocol_v2` passthrough feature is removed
because the crate has no v2-specific code. Enable `unstable_protocol_v2` on
`agent-client-protocol` instead.

Then add `rmcp_3::` to every import from the crate:

Expand All @@ -790,15 +804,78 @@ Then add `rmcp_3::` to every import from the crate:
| `agent_client_protocol_rmcp::McpServerExt` | `agent_client_protocol_rmcp::rmcp_3::McpServerExt` |
| `agent_client_protocol_rmcp::McpServerBuilder` | `agent_client_protocol_rmcp::rmcp_3::McpServerBuilder` |
| `agent_client_protocol_rmcp::McpTool` | `agent_client_protocol_rmcp::rmcp_3::McpTool` |
| `agent_client_protocol_rmcp::EnabledTools` | `agent_client_protocol_rmcp::rmcp_3::EnabledTools` |
| `agent_client_protocol_rmcp::tool_fn!` | `agent_client_protocol_rmcp::rmcp_3::tool_fn!` |
| `agent_client_protocol_rmcp::tool_fn_mut!` | `agent_client_protocol_rmcp::rmcp_3::tool_fn_mut!` |

`McpTool`, `EnabledTools`, `tool_fn!` and `tool_fn_mut!` are re-exports of the
`McpTool`, `tool_fn!` and `tool_fn_mut!` are re-exports of the
core crate's items, which you can also import from
`agent_client_protocol::mcp_server` and `agent_client_protocol` directly.

Future rmcp major versions arrive as new features and modules (such as
`rmcp_4`) in minor releases, so you can enable both while moving your services
to the new rmcp major version. Removing support for an rmcp major version is a
breaking change and only happens in an ACP major release.

### Other rmcp and `mcp_server` changes

- `EnabledTools` is no longer public. No public API took or returned it. Use
`enable_tool`, `disable_tool`, `enable_all_tools`, and `disable_all_tools`.
- `McpServerBuilder::enable_tool` / `disable_tool` and
`McpToolRegistry::enable_tool` / `disable_tool` return `UnknownToolError`
instead of a `-32600` `Error`. `?` still converts it into an `Error`.
- `McpServerExt` is sealed. It was only meant to be implemented for
`McpServer`.
- `McpConnectionContext::Acp` is `#[non_exhaustive]`. Match it as
`McpConnectionContext::Acp { server_id, .. }`.
- Builder-built servers report the builder name and `McpServerBuilder::version`
(default `0.0.0`) as their MCP `serverInfo`, not `rmcp`.
- An error from a builder tool, including arguments that do not match its input
schema, is a `tools/call` result with `isError: true` instead of a JSON-RPC
error. A call to an unknown or disabled tool still gets `-32602`.
`RegisteredMcpTool::call_tool` reports bad arguments as `-32602` instead of
`-32603`.
- The native MCP-over-ACP path for `McpServer::from_rmcp` and builder servers
follows the core crate's `unstable_mcp_over_acp` feature. Before, enabling
the feature only on `agent-client-protocol` made each request bridge a new
rmcp service.

## HTTP transport

`agent-client-protocol-http` narrows its public types:

- `HttpClientError` is `#[non_exhaustive]`, so matches need a wildcard arm.
`InvalidUrl` and `Reqwest` no longer implement `From`, and their `Display`
text no longer repeats the underlying error. Walk
`std::error::Error::source` to see it. `InvalidUrl` now holds an opaque
`InvalidUrlError` instead of `url::ParseError`; it displays the parser's
message.
- `CorsOptions` is an opaque struct. The `Disabled`, `AllowOrigins`, and
`AllowAnyOrigin` variants are gone. Use `CorsOptions::disabled()`,
`CorsOptions::allow_origins(...)`, and `CorsOptions::allow_any_origin()`, and
compare policies with `==`.
- `CorsOptions::allow_origins` returns `InvalidOriginError` instead of
`axum::http::header::InvalidHeaderValue`. It names the rejected origin
through `origin()`.

The crate does not re-export `axum`, `reqwest`, or `url`. Only two of its
signatures use third-party types: `AcpHttpServer::into_router` returns an
`axum::Router`, and `HttpClientBuilder::configure_http` and
`HttpClient::from_http_client` take reqwest types. Depend on matching
versions of those crates to use them.

## `AcpAgent`

- `AcpAgent::spawn_process` is no longer public. It returned `async_process`
types and skipped the process-group teardown that `ConnectTo` installs.
Connect the `AcpAgent` itself instead.
- `AcpAgent::claude_agent()` and `AcpAgent::codex()` are removed. They ran
`npx -y ...@latest`, which installs whatever version was published last.
Name the adapter version yourself:

```rust
use std::str::FromStr;
use agent_client_protocol::AcpAgent;

let agent = AcpAgent::from_str("npx -y @agentclientprotocol/claude-agent-acp@<version>")?;
# Ok::<(), agent_client_protocol::Error>(())
```
6 changes: 6 additions & 0 deletions md/transport-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ the target does not imply a JavaScript host, other OS-less WebAssembly hosts
must arrange a compatible UUID randomness backend instead. This crate does not
provide a WebAssembly executor or host I/O adapter.

Handlers, transports, and connection futures must be `Send` on every target,
including WebAssembly. JavaScript host values such as `wasm_bindgen::JsValue`
are `!Send`, so keep them outside the connection and exchange `Send` data with
it through `Channel` or `ByteStreams`. A later release may relax these bounds
on WebAssembly. That change would not affect native code.

The rmcp integration library also compiles for WASI with its `rmcp_3` feature,
alone or with its MCP-over-ACP features. It uses Tokio's async I/O utilities and
single-thread-capable runtime without requiring native stdio or a multithreaded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,8 @@ async fn test_trace_mcp_tool_call() -> Result<(), agent_client_protocol::Error>
"cacheScope": String("private"),
"_meta": Object {
"io.modelcontextprotocol/serverInfo": Object {
"name": String("rmcp"),
"version": String("<rmcp version>"),
"name": String("test"),
"version": String("0.0.0"),
},
},
},
Expand Down
70 changes: 47 additions & 23 deletions src/agent-client-protocol-cookbook/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,30 +362,44 @@ pub mod connecting_as_client {
//! # Handling Permission Requests
//!
//! Agents may send [`RequestPermissionRequest`] to ask for user approval
//! before taking actions. Handle these with [`on_receive_request`]:
//! before taking actions. Handle these with [`on_receive_request`]. Answer
//! `cancelled` only after cancelling the turn with `session/cancel`:
//!
//! ```ignore
//! Client.v1()
//! .on_receive_request(async |req: RequestPermissionRequest, responder, _connection| {
//! // Auto-approve (YOLO mode) by option `kind`, never by position
//! // or name: the agent decides which options exist and in what order.
//! let allow = req.options.iter().find(|option| {
//! matches!(
//! option.kind,
//! PermissionOptionKind::AllowOnce | PermissionOptionKind::AllowAlways
//! )
//! });
//! responder.respond(RequestPermissionResponse::new(
//! match allow {
//! ```
//! use agent_client_protocol::schema::v1::{
//! CancelNotification, PermissionOptionKind, RequestPermissionOutcome,
//! RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome,
//! };
//! use agent_client_protocol::{Client, ConnectTo};
//!
//! async fn run(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
//! Client.v1()
//! .on_receive_request(async |req: RequestPermissionRequest, responder, connection| {
//! // Auto-approve (YOLO mode) by option `kind`, never by position
//! // or name: the agent decides which options exist and in what order.
//! let allow = req.options.iter().find(|option| {
//! matches!(
//! option.kind,
//! PermissionOptionKind::AllowOnce | PermissionOptionKind::AllowAlways
//! )
//! });
//! let outcome = match allow {
//! Some(option) => RequestPermissionOutcome::Selected(
//! SelectedPermissionOutcome::new(option.option_id.clone()),
//! ),
//! None => RequestPermissionOutcome::Cancelled,
//! }
//! ))
//! }, agent_client_protocol::on_receive_request!())
//! .connect_with(transport, async |connection| { /* ... */ })
//! .await
//! // `cancelled` is only valid after cancelling the turn.
//! None => {
//! connection.send_notification(CancelNotification::new(
//! req.session_id.clone(),
//! ))?;
//! RequestPermissionOutcome::Cancelled
//! }
//! };
//! responder.respond(RequestPermissionResponse::new(outcome))
//! }, agent_client_protocol::on_receive_request!())
//! .connect_with(transport, async |_connection| Ok(()))
//! .await
//! }
//! ```
//!
//! # Note on `block_task`
Expand Down Expand Up @@ -623,6 +637,11 @@ pub mod custom_message_handlers {
//! }
//! ```
//!
//! `Dispatch` is the raw, version-neutral message, so a handler decides
//! which protocol version's types to parse it as. This one parses v1
//! `InitializeRequest`s, so install it only on a connection that speaks
//! ACP v1. A v2 handler would parse `schema::v2` types instead.
//!
//! # When to use `MatchDispatch` vs `MatchDispatchFrom`
//!
//! - [`MatchDispatch`] - Use when you don't need peer-aware handling
Expand Down Expand Up @@ -1059,13 +1078,16 @@ pub mod filtering_tools {
//! // Only enable the safe tool
//! .enable_tool("safe")
//! .map(|b| b.build())
//! .map_err(agent_client_protocol::Error::from)
//! }
//! ```
//!
//! # Error handling
//!
//! Both [`enable_tool`] and [`disable_tool`] return `Result` and will error
//! if the tool name doesn't match any registered tool. This helps catch typos:
//! Both [`enable_tool`] and [`disable_tool`] return an [`UnknownToolError`]
//! if the tool name doesn't match any registered tool. This helps catch typos.
//! It is a local error, and `?` converts it into an
//! [`agent_client_protocol::Error`]:
//!
//! ```
//! use agent_client_protocol::mcp_server::McpServer;
Expand All @@ -1076,7 +1098,8 @@ pub mod filtering_tools {
//! let result = McpServer::<Conductor, _>::builder("server")
//! .disable_tool("ech"); // Typo! Should be "echo"
//!
//! assert!(result.is_err());
//! let Err(error) = result else { panic!("expected an unknown tool") };
//! assert_eq!(error.name(), "ech");
//! ```
//!
//! Calling enable/disable on an already enabled/disabled tool is not an error -
Expand All @@ -1085,6 +1108,7 @@ pub mod filtering_tools {
//! [`disable_tool`]: agent_client_protocol_rmcp::rmcp_3::McpServerBuilder::disable_tool
//! [`enable_tool`]: agent_client_protocol_rmcp::rmcp_3::McpServerBuilder::enable_tool
//! [`disable_all_tools`]: agent_client_protocol_rmcp::rmcp_3::McpServerBuilder::disable_all_tools
//! [`UnknownToolError`]: agent_client_protocol_rmcp::rmcp_3::UnknownToolError
}

pub mod running_proxies_with_conductor {
Expand Down
2 changes: 2 additions & 0 deletions src/agent-client-protocol-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ reqwest = { workspace = true, features = ["stream"], optional = true }
url = { workspace = true, optional = true }

[dev-dependencies]
# The `AcpHttpServer` example serves v1 and v2 through `AgentProtocolRouter`.
agent-client-protocol = { workspace = true, features = ["unstable_protocol_v2"] }
async-stream.workspace = true
axum = { workspace = true, features = ["ws", "json"] }
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
Expand Down
Loading
Loading