use std::{marker::PhantomData, sync::Arc};
use futures::{StreamExt, channel::mpsc};
use crate::{
ConnectTo, Dispatch, DynConnectTo, Role,
jsonrpc::run::{NullRun, RunWithConnectionTo},
mcp_server::{McpConnectionContext, McpConnectionTo, McpServerConnect},
role,
};
#[cfg(feature = "unstable_mcp_over_acp")]
use uuid::Uuid;
#[cfg(feature = "unstable_mcp_over_acp")]
use crate::{
Agent, Client, ConnectionTo, HandleDispatchFrom, Handled,
jsonrpc::DynamicHandlerGuard,
mcp_server::active_session::{McpActiveSession, V1McpProtocol},
schema::v1::{
LoadSessionRequest, McpServer as SchemaMcpServer, McpServerAcp, McpServerAcpId,
NewSessionRequest, ResumeSessionRequest,
},
util::MatchDispatchFrom,
};
#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
use crate::{JsonRpcMessage, UntypedMessage};
#[cfg(feature = "unstable_mcp_over_acp")]
use crate::role::HasPeer;
#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_session_fork"))]
use crate::schema::v1::ForkSessionRequest;
pub struct McpServer<Counterpart: Role, Run = NullRun> {
phantom: PhantomData<Counterpart>,
connect: Arc<dyn McpServerConnect<Counterpart>>,
runner: Run,
}
impl<Counterpart: Role + std::fmt::Debug, Run: std::fmt::Debug> std::fmt::Debug
for McpServer<Counterpart, Run>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("McpServer")
.field("phantom", &self.phantom)
.field("runner", &self.runner)
.finish_non_exhaustive()
}
}
impl<Counterpart: Role, Run> McpServer<Counterpart, Run>
where
Run: RunWithConnectionTo<Counterpart>,
{
pub fn new(c: impl McpServerConnect<Counterpart>, runner: Run) -> Self {
McpServer {
phantom: PhantomData,
connect: Arc::new(c),
runner,
}
}
#[cfg(feature = "unstable_mcp_over_acp")]
pub(crate) fn into_handler_and_runner(self) -> (McpSessionHandler<Counterpart>, Run)
where
Counterpart: HasPeer<Agent>,
{
let Self {
phantom: _,
connect,
runner,
} = self;
let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
(McpSessionHandler::new(server_id, connect), runner)
}
#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
pub(crate) fn into_v2_handler_and_runner(self) -> (V2McpSessionHandler<Counterpart>, Run)
where
Counterpart: HasPeer<Agent>,
{
let Self {
phantom: _,
connect,
runner,
} = self;
let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
(V2McpSessionHandler::new(server_id, connect), runner)
}
}
#[cfg(feature = "unstable_mcp_over_acp")]
pub(crate) struct McpSessionHandler<Counterpart: Role>
where
Counterpart: HasPeer<Agent>,
{
server_id: McpServerAcpId,
connect: Arc<dyn McpServerConnect<Counterpart>>,
active_session: McpActiveSession<Counterpart, V1McpProtocol>,
}
#[cfg(feature = "unstable_mcp_over_acp")]
impl<Counterpart: Role> McpSessionHandler<Counterpart>
where
Counterpart: HasPeer<Agent>,
{
pub fn new(server_id: McpServerAcpId, connect: Arc<dyn McpServerConnect<Counterpart>>) -> Self {
Self {
active_session: McpActiveSession::new(server_id.clone(), connect.clone()),
server_id,
connect,
}
}
fn append_declaration(&self, mcp_servers: &mut Vec<SchemaMcpServer>) {
mcp_servers.push(SchemaMcpServer::Acp(McpServerAcp::new(
self.connect.name(),
self.server_id.clone(),
)));
}
}
#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
pub(crate) struct V2McpSessionHandler<Counterpart: Role>
where
Counterpart: HasPeer<Agent>,
{
server_id: McpServerAcpId,
connect: Arc<dyn McpServerConnect<Counterpart>>,
active_session: McpActiveSession<Counterpart, crate::mcp_server::active_session::V2McpProtocol>,
}
#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
impl<Counterpart: Role> V2McpSessionHandler<Counterpart>
where
Counterpart: HasPeer<Agent>,
{
fn new(server_id: McpServerAcpId, connect: Arc<dyn McpServerConnect<Counterpart>>) -> Self {
Self {
active_session: McpActiveSession::new(server_id.clone(), connect.clone()),
server_id,
connect,
}
}
fn declaration(&self) -> crate::schema::v2::McpServer {
crate::schema::v2::McpServer::Acp(crate::schema::v2::McpServerAcp::new(
self.connect.name(),
crate::schema::v2::McpServerAcpId::from(self.server_id.0.clone()),
))
}
fn append_declaration(&self, mcp_servers: &mut Vec<crate::schema::v2::McpServer>) {
mcp_servers.push(self.declaration());
}
fn validate_session_setup(request: &UntypedMessage) -> Result<bool, crate::Error> {
match request.method() {
"session/new" => {
crate::schema::v2::NewSessionRequest::parse_message(
request.method(),
request.params(),
)?;
Ok(true)
}
"session/resume" => {
crate::schema::v2::ResumeSessionRequest::parse_message(
request.method(),
request.params(),
)?;
Ok(true)
}
#[cfg(feature = "unstable_session_fork")]
"session/fork" => {
crate::schema::v2::ForkSessionRequest::parse_message(
request.method(),
request.params(),
)?;
Ok(true)
}
_ => Ok(false),
}
}
fn append_declaration_to_raw(&self, request: &mut UntypedMessage) -> Result<(), crate::Error> {
let serde_json::Value::Object(params) = &mut request.params else {
return Err(
crate::Error::invalid_params().data("session setup parameters must be an object")
);
};
let declaration =
serde_json::to_value(self.declaration()).map_err(crate::Error::into_internal_error)?;
match params.get_mut("mcpServers") {
Some(serde_json::Value::Array(servers)) => servers.push(declaration),
Some(value) => *value = serde_json::Value::Array(vec![declaration]),
None => {
params.insert(
"mcpServers".to_string(),
serde_json::Value::Array(vec![declaration]),
);
}
}
Ok(())
}
pub fn into_dynamic_handler(
self,
mcp_servers: &mut Vec<crate::schema::v2::McpServer>,
cx: &crate::V2ConnectionTo<Counterpart>,
) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error> {
self.append_declaration(mcp_servers);
cx.add_dynamic_handler(self.active_session)
}
}
#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for V2McpSessionHandler<Counterpart>
where
Counterpart: HasPeer<Client> + HasPeer<Agent>,
{
async fn handle_dispatch_from(
&mut self,
message: Dispatch,
cx: ConnectionTo<Counterpart>,
) -> Result<Handled<Dispatch>, crate::Error> {
MatchDispatchFrom::new(message, &cx)
.if_request_from(Client, async |mut request: UntypedMessage, responder| {
if !Self::validate_session_setup(&request)? {
return Ok(Handled::No {
message: (request, responder),
retry: false,
});
}
self.append_declaration_to_raw(&mut request)?;
Ok(Handled::No {
message: (request, responder),
retry: false,
})
})
.await
.otherwise_delegate(&mut self.active_session)
.await
}
fn describe_chain(&self) -> impl std::fmt::Debug {
format!("V2McpServer({})", self.connect.name())
}
}
#[cfg(feature = "unstable_mcp_over_acp")]
impl<Counterpart: Role> McpSessionHandler<Counterpart>
where
Counterpart: HasPeer<Agent>,
{
pub fn into_dynamic_handler(
self,
request: &mut NewSessionRequest,
cx: &ConnectionTo<Counterpart>,
) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error>
where
Counterpart: HasPeer<Agent>,
{
self.append_declaration(&mut request.mcp_servers);
cx.add_dynamic_handler(self.active_session)
}
}
#[cfg(feature = "unstable_mcp_over_acp")]
impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for McpSessionHandler<Counterpart>
where
Counterpart: HasPeer<Client> + HasPeer<Agent>,
{
async fn handle_dispatch_from(
&mut self,
message: Dispatch,
cx: ConnectionTo<Counterpart>,
) -> Result<Handled<Dispatch>, crate::Error> {
let matcher = MatchDispatchFrom::new(message, &cx)
.if_request_from(Client, async |mut request: NewSessionRequest, responder| {
self.append_declaration(&mut request.mcp_servers);
Ok(Handled::No {
message: (request, responder),
retry: false,
})
})
.await
.if_request_from(
Client,
async |mut request: LoadSessionRequest, responder| {
self.append_declaration(&mut request.mcp_servers);
Ok(Handled::No {
message: (request, responder),
retry: false,
})
},
)
.await
.if_request_from(
Client,
async |mut request: ResumeSessionRequest, responder| {
self.append_declaration(&mut request.mcp_servers);
Ok(Handled::No {
message: (request, responder),
retry: false,
})
},
)
.await;
#[cfg(feature = "unstable_session_fork")]
let matcher = matcher
.if_request_from(
Client,
async |mut request: ForkSessionRequest, responder| {
self.append_declaration(&mut request.mcp_servers);
Ok(Handled::No {
message: (request, responder),
retry: false,
})
},
)
.await;
matcher.otherwise_delegate(&mut self.active_session).await
}
fn describe_chain(&self) -> impl std::fmt::Debug {
format!("McpServer({})", self.connect.name())
}
}
impl<Run> ConnectTo<role::mcp::Client> for McpServer<role::mcp::Client, Run>
where
Run: RunWithConnectionTo<role::mcp::Client> + 'static,
{
async fn connect_to(
self,
client: impl ConnectTo<role::mcp::Server>,
) -> Result<(), crate::Error> {
let Self {
connect,
runner,
phantom: _,
} = self;
let (tx, mut rx) = mpsc::unbounded();
role::mcp::Server
.builder()
.with_runner(runner)
.on_receive_dispatch(
async |message_from_client: Dispatch, _cx| {
tx.unbounded_send(message_from_client)
.map_err(|_| crate::util::internal_error("nobody listening to mcp server"))
},
crate::on_receive_dispatch!(),
)
.with_spawned(async move |connection_to_client| {
let spawned_server: DynConnectTo<role::mcp::Client> =
connect.connect(McpConnectionTo {
context: McpConnectionContext::Standalone,
connection: connection_to_client.clone(),
});
role::mcp::Client
.builder()
.on_receive_dispatch(
async |message_from_server: Dispatch, _| {
connection_to_client.send_proxied_message(message_from_server)
},
crate::on_receive_dispatch!(),
)
.connect_with(spawned_server, async |connection_to_server| {
while let Some(message_from_client) = rx.next().await {
connection_to_server.send_proxied_message(message_from_client)?;
}
Ok(())
})
.await
})
.connect_to(client)
.await
}
}
#[cfg(all(
test,
feature = "unstable_mcp_over_acp",
feature = "unstable_protocol_v2"
))]
mod tests {
use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
use serde::Serialize;
use serde_json::{Value, json};
use super::V2McpSessionHandler;
use crate::{
Conductor, DynConnectTo, Error, UntypedMessage,
mcp_server::{McpConnectionTo, McpServerConnect},
role,
schema::{
v1::McpServerAcpId,
v2::{self, McpServer},
},
};
struct UnusedMcpConnect;
impl McpServerConnect<Conductor> for UnusedMcpConnect {
fn name(&self) -> String {
"global-v2-server".to_owned()
}
fn connect(&self, _context: McpConnectionTo<Conductor>) -> DynConnectTo<role::mcp::Client> {
panic!("declaration tests must not connect to the MCP server")
}
}
fn handler() -> V2McpSessionHandler<Conductor> {
V2McpSessionHandler::new(
McpServerAcpId::new("global-v2-server-id"),
Arc::new(UnusedMcpConnect),
)
}
fn existing_server() -> McpServer {
McpServer::Other(v2::OtherMcpServer::new(
"_future_transport",
BTreeMap::from([("futureOption".to_owned(), json!({ "nested": true }))]),
))
}
fn meta() -> v2::Meta {
v2::Meta::from_iter([("preserved".to_owned(), json!({ "nested": true }))])
}
fn assert_raw_append_preserves_params(
handler: &V2McpSessionHandler<Conductor>,
method: &str,
params: impl Serialize,
) -> Result<v2::McpServerAcpId, Error> {
let mut params = serde_json::to_value(params)?;
let Value::Object(params_object) = &mut params else {
panic!("session setup params should serialize as an object");
};
params_object.insert(
"_futureSessionField".to_owned(),
json!({ "must": ["remain", "untouched"] }),
);
let mut expected = params.clone();
expected
.get_mut("mcpServers")
.and_then(Value::as_array_mut)
.expect("test request should contain mcpServers")
.push(serde_json::to_value(handler.declaration())?);
let mut request = UntypedMessage::new(method, params)?;
assert!(V2McpSessionHandler::<Conductor>::validate_session_setup(
&request
)?);
handler.append_declaration_to_raw(&mut request)?;
assert_eq!(
request.params, expected,
"global attachment must only append its declaration"
);
let appended = request
.params
.get("mcpServers")
.and_then(Value::as_array)
.and_then(|servers| servers.last())
.cloned()
.expect("global declaration should be appended");
match serde_json::from_value::<McpServer>(appended)? {
McpServer::Acp(server) => {
assert_eq!(server.name, "global-v2-server");
Ok(server.server_id)
}
server => panic!("expected an ACP server declaration, got {server:?}"),
}
}
#[test]
fn v2_global_mcp_declaration_preserves_all_session_setup_params() -> Result<(), Error> {
let handler = handler();
let cwd = PathBuf::from("/tmp/global-v2-mcp");
let additional_directory = PathBuf::from("/tmp/global-v2-mcp-additional");
let session_id = v2::SessionId::new("session-to-resume");
let existing_server = existing_server();
let new_server_id = assert_raw_append_preserves_params(
&handler,
"session/new",
v2::NewSessionRequest::new(cwd.clone())
.additional_directories([additional_directory.clone()])
.mcp_servers(vec![existing_server.clone()])
.meta(meta()),
)?;
let resume_server_id = assert_raw_append_preserves_params(
&handler,
"session/resume",
v2::ResumeSessionRequest::new(session_id.clone(), cwd.clone())
.additional_directories([additional_directory.clone()])
.mcp_servers(vec![existing_server.clone()])
.replay_from(v2::ReplayFrom::Start(
v2::ReplayFromStart::new().meta(meta()),
))
.meta(meta()),
)?;
assert_eq!(resume_server_id, new_server_id);
#[cfg(feature = "unstable_session_fork")]
{
let fork_server_id = assert_raw_append_preserves_params(
&handler,
"session/fork",
v2::ForkSessionRequest::new(session_id, cwd)
.additional_directories([additional_directory])
.mcp_servers(vec![existing_server])
.meta(meta()),
)?;
assert_eq!(fork_server_id, new_server_id);
}
Ok(())
}
#[test]
fn v2_global_mcp_handler_ignores_non_setup_requests() -> Result<(), Error> {
let request = UntypedMessage::new(
"session/prompt",
json!({
"sessionId": "session-to-prompt",
"prompt": []
}),
)?;
assert!(!V2McpSessionHandler::<Conductor>::validate_session_setup(
&request
)?);
assert_eq!(request.method(), "session/prompt");
Ok(())
}
}