Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,55 @@ struct ProxyWithMcpAndHandler {
config: Arc<HandlerConfig>,
}

struct RestoredSessionMcpProxy;

fn restored_session_server()
-> McpServer<Conductor, impl agent_client_protocol::RunWithConnectionTo<Conductor>> {
McpServer::builder("test-server")
.tool_fn_mut(
"echo",
"Echoes back the input",
async |params: EchoParams, _cx| {
Ok(EchoOutput {
result: format!("Echo: {}", params.message),
})
},
agent_client_protocol::tool_fn_mut!(),
)
.build()
}

impl ConnectTo<Conductor> for RestoredSessionMcpProxy {
async fn connect_to(
self,
client: impl ConnectTo<Proxy>,
) -> Result<(), agent_client_protocol::Error> {
Proxy
.builder()
.name("restored-session-mcp")
.on_receive_request_from(
Client,
async |request: LoadSessionRequest, responder, cx| {
cx.build_restored_session_from(request)
.with_mcp_server(restored_session_server())?
.on_proxy_session_start(responder, async |_session_id| Ok(()))
},
agent_client_protocol::on_receive_request!(),
)
.on_receive_request_from(
Client,
async |request: ResumeSessionRequest, responder, cx| {
cx.build_restored_session_from(request)
.with_mcp_server(restored_session_server())?
.on_proxy_session_start(responder, async |_session_id| Ok(()))
},
agent_client_protocol::on_receive_request!(),
)
.connect_to(client)
.await
}
}

impl ConnectTo<Conductor> for ProxyWithMcpAndHandler {
async fn connect_to(
self,
Expand Down Expand Up @@ -324,3 +373,43 @@ async fn test_mcp_server_injected_into_all_session_setup_requests()

Ok(())
}

/// Restored sessions can each receive an independently scoped MCP server.
#[tokio::test]
async fn test_per_session_mcp_server_attached_to_load_and_resume()
-> Result<(), agent_client_protocol::Error> {
let setup_requests = Arc::new(SetupRequests::default());
let proxy = DynConnectTo::<Conductor>::new(RestoredSessionMcpProxy);
let agent = DynConnectTo::<Client>::new(SimpleAgent {
setup_requests: setup_requests.clone(),
});

run_test(vec![proxy], agent, async |connection_to_editor| {
recv(connection_to_editor.send_request(InitializeRequest::new(ProtocolVersion::V1)))
.await?;

let cwd = PathBuf::from("/tmp");
recv(connection_to_editor.send_request(LoadSessionRequest::new(
SessionId::new("loaded-session"),
cwd.clone(),
)))
.await?;
recv(connection_to_editor.send_request(ResumeSessionRequest::new(
SessionId::new("resumed-session"),
cwd,
)))
.await?;

Ok(())
})
.await?;

let server_ids = setup_requests.server_ids.lock().unwrap();
assert_eq!(server_ids.len(), 2);
assert_ne!(
server_ids[0], server_ids[1],
"each restored session should advertise its own MCP server ID"
);

Ok(())
}
25 changes: 25 additions & 0 deletions src/agent-client-protocol-cookbook/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,31 @@ pub mod per_session_mcp_server {
//! ID-keyed state, preinstall a gate or placeholder that later handlers
//! await, then populate it from the callback.
//!
//! # Restoring stable v1 sessions
//!
//! `session/load` and `session/resume` can attach a new per-session server
//! through the corresponding restore builder:
//!
//! ```rust,ignore
//! Proxy.builder()
//! .on_receive_request_from(
//! Client,
//! async |request: LoadSessionRequest, responder, connection| {
//! connection
//! .build_restored_session_from(request)
//! .with_mcp_server(build_workspace_server())?
//! .on_proxy_session_start(responder, async |session_id| {
//! tracing::info!(%session_id, "Session restored");
//! Ok(())
//! })
//! },
//! agent_client_protocol::on_receive_request!(),
//! );
//! ```
//!
//! Use the same pattern with `ResumeSessionRequest`. Each invocation gets
//! its own MCP server ID and SDK-managed session route.
//!
//! # Draft v2 pattern
//!
//! `Proxy.v2()` exposes the same non-blocking setup shape with v2 schema
Expand Down
3 changes: 3 additions & 0 deletions src/agent-client-protocol/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Added

- *(unstable)* Add `ConnectionTo::build_restored_session_from` for attaching
per-session MCP servers while proxying stable-v1 `session/load` and
`session/resume` requests.
- *(unstable)* Expose programmatic tool-call names through the
`unstable_tool_call_name` feature.
- *(unstable)* Expose v1 and draft-v2 plan operations through the
Expand Down
23 changes: 23 additions & 0 deletions src/agent-client-protocol/src/concepts/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@
//!
//! See the cookbook for detailed MCP server examples.
//!
//! A proxy restoring a v1 session can attach a fresh MCP server to that
//! session with `build_restored_session_from`. The same builder accepts
//! `LoadSessionRequest` and `ResumeSessionRequest`:
//!
//! ```rust,ignore
//! Proxy.builder().on_receive_request_from(
//! Client,
//! async |request: LoadSessionRequest, responder, cx| {
//! cx.build_restored_session_from(request)
//! .with_mcp_server(session_tools())?
//! .on_proxy_session_start(responder, async |session_id| {
//! track_session(session_id);
//! Ok(())
//! })
//! },
//! agent_client_protocol::on_receive_request!(),
//! );
//! ```
//!
//! The SDK installs session routing and the MCP handler before forwarding the
//! restore request. Successful handlers remain active for the connection
//! lifetime; failed restore requests remove the pending handlers.
//!
//! # Non-Blocking Session Start
//!
//! If you're inside an `on_receive_*` callback and need to start a session,
Expand Down
28 changes: 25 additions & 3 deletions src/agent-client-protocol/src/mcp_server/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ use crate::{
role,
};

#[cfg(feature = "unstable_mcp_over_acp")]
pub(crate) trait McpSessionSetupRequest {
fn mcp_servers_mut(&mut self) -> &mut Vec<SchemaMcpServer>;
}

#[cfg(feature = "unstable_mcp_over_acp")]
macro_rules! impl_mcp_session_setup_request {
($($request:ty),+ $(,)?) => {
$(
impl McpSessionSetupRequest for $request {
fn mcp_servers_mut(&mut self) -> &mut Vec<SchemaMcpServer> {
&mut self.mcp_servers
}
}
)+
};
}

#[cfg(feature = "unstable_mcp_over_acp")]
impl_mcp_session_setup_request!(NewSessionRequest, LoadSessionRequest, ResumeSessionRequest);

#[cfg(feature = "unstable_mcp_over_acp")]
use uuid::Uuid;

Expand Down Expand Up @@ -313,15 +334,16 @@ where
/// will no longer be received, so you need to keep this value alive as long as the session
/// is in use. You can also invoke [`DynamicHandlerGuard::detach`] if you
/// want to keep the handler registered for the life of the connection.
pub fn into_dynamic_handler(
pub fn into_dynamic_handler<Request>(
self,
request: &mut NewSessionRequest,
request: &mut Request,
cx: &ConnectionTo<Counterpart>,
) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error>
where
Counterpart: HasPeer<Agent>,
Request: McpSessionSetupRequest,
{
self.append_declaration(&mut request.mcp_servers);
self.append_declaration(request.mcp_servers_mut());
cx.add_dynamic_handler(self.active_session)
}
}
Expand Down
Loading