1use std::{marker::PhantomData, sync::Arc};
4
5use futures::{StreamExt, channel::mpsc};
6
7use crate::{
8 ConnectTo, Dispatch, DynConnectTo, Role,
9 jsonrpc::run::{NullRun, RunWithConnectionTo},
10 mcp_server::{McpConnectionContext, McpConnectionTo, McpServerConnect},
11 role,
12};
13
14#[cfg(feature = "unstable_mcp_over_acp")]
15use uuid::Uuid;
16
17#[cfg(feature = "unstable_mcp_over_acp")]
18use crate::{
19 Agent, Client, ConnectionTo, HandleDispatchFrom, Handled,
20 jsonrpc::DynamicHandlerGuard,
21 mcp_server::active_session::{McpActiveSession, V1McpProtocol},
22 schema::v1::{
23 LoadSessionRequest, McpServer as SchemaMcpServer, McpServerAcp, McpServerAcpId,
24 NewSessionRequest, ResumeSessionRequest,
25 },
26 util::MatchDispatchFrom,
27};
28
29#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
30use crate::{JsonRpcMessage, UntypedMessage};
31
32#[cfg(feature = "unstable_mcp_over_acp")]
33use crate::role::HasPeer;
34
35#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_session_fork"))]
36use crate::schema::v1::ForkSessionRequest;
37
38pub struct McpServer<Counterpart: Role, Run = NullRun> {
64 phantom: PhantomData<Counterpart>,
66
67 connect: Arc<dyn McpServerConnect<Counterpart>>,
69
70 runner: Run,
79}
80
81impl<Counterpart: Role + std::fmt::Debug, Run: std::fmt::Debug> std::fmt::Debug
82 for McpServer<Counterpart, Run>
83{
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("McpServer")
86 .field("phantom", &self.phantom)
87 .field("runner", &self.runner)
88 .finish_non_exhaustive()
89 }
90}
91
92impl<Counterpart: Role, Run> McpServer<Counterpart, Run>
93where
94 Run: RunWithConnectionTo<Counterpart>,
95{
96 pub fn new(c: impl McpServerConnect<Counterpart>, runner: Run) -> Self {
103 McpServer {
104 phantom: PhantomData,
105 connect: Arc::new(c),
106 runner,
107 }
108 }
109
110 #[cfg(feature = "unstable_mcp_over_acp")]
112 pub(crate) fn into_handler_and_runner(self) -> (McpSessionHandler<Counterpart>, Run)
113 where
114 Counterpart: HasPeer<Agent>,
115 {
116 let Self {
117 phantom: _,
118 connect,
119 runner,
120 } = self;
121 let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
122 (McpSessionHandler::new(server_id, connect), runner)
123 }
124
125 #[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
127 pub(crate) fn into_v2_handler_and_runner(self) -> (V2McpSessionHandler<Counterpart>, Run)
128 where
129 Counterpart: HasPeer<Agent>,
130 {
131 let Self {
132 phantom: _,
133 connect,
134 runner,
135 } = self;
136 let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
137 (V2McpSessionHandler::new(server_id, connect), runner)
138 }
139}
140
141#[cfg(feature = "unstable_mcp_over_acp")]
143pub(crate) struct McpSessionHandler<Counterpart: Role>
144where
145 Counterpart: HasPeer<Agent>,
146{
147 server_id: McpServerAcpId,
148 connect: Arc<dyn McpServerConnect<Counterpart>>,
149 active_session: McpActiveSession<Counterpart, V1McpProtocol>,
150}
151
152#[cfg(feature = "unstable_mcp_over_acp")]
153impl<Counterpart: Role> McpSessionHandler<Counterpart>
154where
155 Counterpart: HasPeer<Agent>,
156{
157 pub fn new(server_id: McpServerAcpId, connect: Arc<dyn McpServerConnect<Counterpart>>) -> Self {
158 Self {
159 active_session: McpActiveSession::new(server_id.clone(), connect.clone()),
160 server_id,
161 connect,
162 }
163 }
164
165 fn append_declaration(&self, mcp_servers: &mut Vec<SchemaMcpServer>) {
167 mcp_servers.push(SchemaMcpServer::Acp(McpServerAcp::new(
168 self.connect.name(),
169 self.server_id.clone(),
170 )));
171 }
172}
173
174#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
176pub(crate) struct V2McpSessionHandler<Counterpart: Role>
177where
178 Counterpart: HasPeer<Agent>,
179{
180 server_id: McpServerAcpId,
181 connect: Arc<dyn McpServerConnect<Counterpart>>,
182 active_session: McpActiveSession<Counterpart, crate::mcp_server::active_session::V2McpProtocol>,
183}
184
185#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
186impl<Counterpart: Role> V2McpSessionHandler<Counterpart>
187where
188 Counterpart: HasPeer<Agent>,
189{
190 fn new(server_id: McpServerAcpId, connect: Arc<dyn McpServerConnect<Counterpart>>) -> Self {
191 Self {
192 active_session: McpActiveSession::new(server_id.clone(), connect.clone()),
193 server_id,
194 connect,
195 }
196 }
197
198 fn declaration(&self) -> crate::schema::v2::McpServer {
199 crate::schema::v2::McpServer::Acp(crate::schema::v2::McpServerAcp::new(
200 self.connect.name(),
201 crate::schema::v2::McpServerAcpId::from(self.server_id.0.clone()),
202 ))
203 }
204
205 fn append_declaration(&self, mcp_servers: &mut Vec<crate::schema::v2::McpServer>) {
206 mcp_servers.push(self.declaration());
207 }
208
209 fn validate_session_setup(request: &UntypedMessage) -> Result<bool, crate::Error> {
210 match request.method() {
211 "session/new" => {
212 crate::schema::v2::NewSessionRequest::parse_message(
213 request.method(),
214 request.params(),
215 )?;
216 Ok(true)
217 }
218 "session/resume" => {
219 crate::schema::v2::ResumeSessionRequest::parse_message(
220 request.method(),
221 request.params(),
222 )?;
223 Ok(true)
224 }
225 #[cfg(feature = "unstable_session_fork")]
226 "session/fork" => {
227 crate::schema::v2::ForkSessionRequest::parse_message(
228 request.method(),
229 request.params(),
230 )?;
231 Ok(true)
232 }
233 _ => Ok(false),
234 }
235 }
236
237 fn append_declaration_to_raw(&self, request: &mut UntypedMessage) -> Result<(), crate::Error> {
238 let serde_json::Value::Object(params) = &mut request.params else {
239 return Err(
240 crate::Error::invalid_params().data("session setup parameters must be an object")
241 );
242 };
243 let declaration =
244 serde_json::to_value(self.declaration()).map_err(crate::Error::into_internal_error)?;
245 match params.get_mut("mcpServers") {
246 Some(serde_json::Value::Array(servers)) => servers.push(declaration),
247 Some(value) => *value = serde_json::Value::Array(vec![declaration]),
248 None => {
249 params.insert(
250 "mcpServers".to_string(),
251 serde_json::Value::Array(vec![declaration]),
252 );
253 }
254 }
255 Ok(())
256 }
257
258 pub fn into_dynamic_handler(
260 self,
261 mcp_servers: &mut Vec<crate::schema::v2::McpServer>,
262 cx: &crate::V2ConnectionTo<Counterpart>,
263 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error> {
264 self.append_declaration(mcp_servers);
265 cx.add_dynamic_handler(self.active_session)
266 }
267}
268
269#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
270impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for V2McpSessionHandler<Counterpart>
271where
272 Counterpart: HasPeer<Client> + HasPeer<Agent>,
273{
274 async fn handle_dispatch_from(
275 &mut self,
276 message: Dispatch,
277 cx: ConnectionTo<Counterpart>,
278 ) -> Result<Handled<Dispatch>, crate::Error> {
279 MatchDispatchFrom::new(message, &cx)
280 .if_request_from(Client, async |mut request: UntypedMessage, responder| {
281 if !Self::validate_session_setup(&request)? {
282 return Ok(Handled::No {
283 message: (request, responder),
284 retry: false,
285 });
286 }
287
288 self.append_declaration_to_raw(&mut request)?;
289 Ok(Handled::No {
290 message: (request, responder),
291 retry: false,
292 })
293 })
294 .await
295 .otherwise_delegate(&mut self.active_session)
296 .await
297 }
298
299 fn describe_chain(&self) -> impl std::fmt::Debug {
300 format!("V2McpServer({})", self.connect.name())
301 }
302}
303
304#[cfg(feature = "unstable_mcp_over_acp")]
305impl<Counterpart: Role> McpSessionHandler<Counterpart>
306where
307 Counterpart: HasPeer<Agent>,
308{
309 pub fn into_dynamic_handler(
320 self,
321 request: &mut NewSessionRequest,
322 cx: &ConnectionTo<Counterpart>,
323 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error>
324 where
325 Counterpart: HasPeer<Agent>,
326 {
327 self.append_declaration(&mut request.mcp_servers);
328 cx.add_dynamic_handler(self.active_session)
329 }
330}
331
332#[cfg(feature = "unstable_mcp_over_acp")]
333impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for McpSessionHandler<Counterpart>
334where
335 Counterpart: HasPeer<Client> + HasPeer<Agent>,
336{
337 async fn handle_dispatch_from(
338 &mut self,
339 message: Dispatch,
340 cx: ConnectionTo<Counterpart>,
341 ) -> Result<Handled<Dispatch>, crate::Error> {
342 let matcher = MatchDispatchFrom::new(message, &cx)
343 .if_request_from(Client, async |mut request: NewSessionRequest, responder| {
344 self.append_declaration(&mut request.mcp_servers);
345 Ok(Handled::No {
346 message: (request, responder),
347 retry: false,
348 })
349 })
350 .await
351 .if_request_from(
352 Client,
353 async |mut request: LoadSessionRequest, responder| {
354 self.append_declaration(&mut request.mcp_servers);
355 Ok(Handled::No {
356 message: (request, responder),
357 retry: false,
358 })
359 },
360 )
361 .await
362 .if_request_from(
363 Client,
364 async |mut request: ResumeSessionRequest, responder| {
365 self.append_declaration(&mut request.mcp_servers);
366 Ok(Handled::No {
367 message: (request, responder),
368 retry: false,
369 })
370 },
371 )
372 .await;
373
374 #[cfg(feature = "unstable_session_fork")]
375 let matcher = matcher
376 .if_request_from(
377 Client,
378 async |mut request: ForkSessionRequest, responder| {
379 self.append_declaration(&mut request.mcp_servers);
380 Ok(Handled::No {
381 message: (request, responder),
382 retry: false,
383 })
384 },
385 )
386 .await;
387
388 matcher.otherwise_delegate(&mut self.active_session).await
389 }
390
391 fn describe_chain(&self) -> impl std::fmt::Debug {
392 format!("McpServer({})", self.connect.name())
393 }
394}
395
396impl<Run> ConnectTo<role::mcp::Client> for McpServer<role::mcp::Client, Run>
397where
398 Run: RunWithConnectionTo<role::mcp::Client> + 'static,
399{
400 async fn connect_to(
401 self,
402 client: impl ConnectTo<role::mcp::Server>,
403 ) -> Result<(), crate::Error> {
404 let Self {
405 connect,
406 runner,
407 phantom: _,
408 } = self;
409
410 let (tx, mut rx) = mpsc::unbounded();
411
412 role::mcp::Server
413 .builder()
414 .with_runner(runner)
415 .on_receive_dispatch(
416 async |message_from_client: Dispatch, _cx| {
417 tx.unbounded_send(message_from_client)
418 .map_err(|_| crate::util::internal_error("nobody listening to mcp server"))
419 },
420 crate::on_receive_dispatch!(),
421 )
422 .with_spawned(async move |connection_to_client| {
423 let spawned_server: DynConnectTo<role::mcp::Client> =
424 connect.connect(McpConnectionTo {
425 context: McpConnectionContext::Standalone,
426 connection: connection_to_client.clone(),
427 });
428
429 role::mcp::Client
430 .builder()
431 .on_receive_dispatch(
432 async |message_from_server: Dispatch, _| {
433 connection_to_client.send_proxied_message(message_from_server)
435 },
436 crate::on_receive_dispatch!(),
437 )
438 .connect_with(spawned_server, async |connection_to_server| {
439 while let Some(message_from_client) = rx.next().await {
440 connection_to_server.send_proxied_message(message_from_client)?;
441 }
442 Ok(())
443 })
444 .await
445 })
446 .connect_to(client)
447 .await
448 }
449}
450
451#[cfg(all(
452 test,
453 feature = "unstable_mcp_over_acp",
454 feature = "unstable_protocol_v2"
455))]
456mod tests {
457 use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
458
459 use serde::Serialize;
460 use serde_json::{Value, json};
461
462 use super::V2McpSessionHandler;
463 use crate::{
464 Conductor, DynConnectTo, Error, UntypedMessage,
465 mcp_server::{McpConnectionTo, McpServerConnect},
466 role,
467 schema::{
468 v1::McpServerAcpId,
469 v2::{self, McpServer},
470 },
471 };
472
473 struct UnusedMcpConnect;
474
475 impl McpServerConnect<Conductor> for UnusedMcpConnect {
476 fn name(&self) -> String {
477 "global-v2-server".to_owned()
478 }
479
480 fn connect(&self, _context: McpConnectionTo<Conductor>) -> DynConnectTo<role::mcp::Client> {
481 panic!("declaration tests must not connect to the MCP server")
482 }
483 }
484
485 fn handler() -> V2McpSessionHandler<Conductor> {
486 V2McpSessionHandler::new(
487 McpServerAcpId::new("global-v2-server-id"),
488 Arc::new(UnusedMcpConnect),
489 )
490 }
491
492 fn existing_server() -> McpServer {
493 McpServer::Other(v2::OtherMcpServer::new(
494 "_future_transport",
495 BTreeMap::from([("futureOption".to_owned(), json!({ "nested": true }))]),
496 ))
497 }
498
499 fn meta() -> v2::Meta {
500 v2::Meta::from_iter([("preserved".to_owned(), json!({ "nested": true }))])
501 }
502
503 fn assert_raw_append_preserves_params(
504 handler: &V2McpSessionHandler<Conductor>,
505 method: &str,
506 params: impl Serialize,
507 ) -> Result<v2::McpServerAcpId, Error> {
508 let mut params = serde_json::to_value(params)?;
509 let Value::Object(params_object) = &mut params else {
510 panic!("session setup params should serialize as an object");
511 };
512 params_object.insert(
513 "_futureSessionField".to_owned(),
514 json!({ "must": ["remain", "untouched"] }),
515 );
516
517 let mut expected = params.clone();
518 expected
519 .get_mut("mcpServers")
520 .and_then(Value::as_array_mut)
521 .expect("test request should contain mcpServers")
522 .push(serde_json::to_value(handler.declaration())?);
523
524 let mut request = UntypedMessage::new(method, params)?;
525 assert!(V2McpSessionHandler::<Conductor>::validate_session_setup(
526 &request
527 )?);
528 handler.append_declaration_to_raw(&mut request)?;
529
530 assert_eq!(
531 request.params, expected,
532 "global attachment must only append its declaration"
533 );
534
535 let appended = request
536 .params
537 .get("mcpServers")
538 .and_then(Value::as_array)
539 .and_then(|servers| servers.last())
540 .cloned()
541 .expect("global declaration should be appended");
542 match serde_json::from_value::<McpServer>(appended)? {
543 McpServer::Acp(server) => {
544 assert_eq!(server.name, "global-v2-server");
545 Ok(server.server_id)
546 }
547 server => panic!("expected an ACP server declaration, got {server:?}"),
548 }
549 }
550
551 #[test]
552 fn v2_global_mcp_declaration_preserves_all_session_setup_params() -> Result<(), Error> {
553 let handler = handler();
554 let cwd = PathBuf::from("/tmp/global-v2-mcp");
555 let additional_directory = PathBuf::from("/tmp/global-v2-mcp-additional");
556 let session_id = v2::SessionId::new("session-to-resume");
557 let existing_server = existing_server();
558
559 let new_server_id = assert_raw_append_preserves_params(
560 &handler,
561 "session/new",
562 v2::NewSessionRequest::new(cwd.clone())
563 .additional_directories([additional_directory.clone()])
564 .mcp_servers(vec![existing_server.clone()])
565 .meta(meta()),
566 )?;
567
568 let resume_server_id = assert_raw_append_preserves_params(
569 &handler,
570 "session/resume",
571 v2::ResumeSessionRequest::new(session_id.clone(), cwd.clone())
572 .additional_directories([additional_directory.clone()])
573 .mcp_servers(vec![existing_server.clone()])
574 .replay_from(v2::ReplayFrom::Start(
575 v2::ReplayFromStart::new().meta(meta()),
576 ))
577 .meta(meta()),
578 )?;
579 assert_eq!(resume_server_id, new_server_id);
580
581 #[cfg(feature = "unstable_session_fork")]
582 {
583 let fork_server_id = assert_raw_append_preserves_params(
584 &handler,
585 "session/fork",
586 v2::ForkSessionRequest::new(session_id, cwd)
587 .additional_directories([additional_directory])
588 .mcp_servers(vec![existing_server])
589 .meta(meta()),
590 )?;
591 assert_eq!(fork_server_id, new_server_id);
592 }
593
594 Ok(())
595 }
596
597 #[test]
598 fn v2_global_mcp_handler_ignores_non_setup_requests() -> Result<(), Error> {
599 let request = UntypedMessage::new(
600 "session/prompt",
601 json!({
602 "sessionId": "session-to-prompt",
603 "prompt": []
604 }),
605 )?;
606
607 assert!(!V2McpSessionHandler::<Conductor>::validate_session_setup(
608 &request
609 )?);
610 assert_eq!(request.method(), "session/prompt");
611 Ok(())
612 }
613}