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 super::McpService;
16#[cfg(feature = "unstable_mcp_over_acp")]
17use uuid::Uuid;
18
19#[cfg(feature = "unstable_mcp_over_acp")]
20use crate::{
21 Agent, Client, ConnectionTo, HandleDispatchFrom, Handled,
22 jsonrpc::DynamicHandlerGuard,
23 mcp_server::active_session::{McpActiveSession, V1McpProtocol},
24 schema::v1::{
25 LoadSessionRequest, McpServer as SchemaMcpServer, McpServerAcp, McpServerAcpId,
26 NewSessionRequest, ResumeSessionRequest,
27 },
28 util::MatchDispatchFrom,
29};
30
31#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
32use crate::{JsonRpcMessage, UntypedMessage};
33
34#[cfg(feature = "unstable_mcp_over_acp")]
35use crate::role::HasPeer;
36
37#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_session_fork"))]
38use crate::schema::v1::ForkSessionRequest;
39
40pub struct McpServer<Counterpart: Role, Run = NullRun> {
66 phantom: PhantomData<Counterpart>,
68
69 connect: Arc<dyn McpServerConnect<Counterpart>>,
71
72 #[cfg(feature = "unstable_mcp_over_acp")]
73 service: Option<Arc<dyn McpService<Counterpart>>>,
74
75 runner: Run,
84}
85
86impl<Counterpart: Role + std::fmt::Debug, Run: std::fmt::Debug> std::fmt::Debug
87 for McpServer<Counterpart, Run>
88{
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("McpServer")
91 .field("phantom", &self.phantom)
92 .field("runner", &self.runner)
93 .finish_non_exhaustive()
94 }
95}
96
97impl<Counterpart: Role, Run> McpServer<Counterpart, Run>
98where
99 Run: RunWithConnectionTo<Counterpart>,
100{
101 pub fn new(c: impl McpServerConnect<Counterpart>, runner: Run) -> Self {
108 McpServer {
109 phantom: PhantomData,
110 connect: Arc::new(c),
111 #[cfg(feature = "unstable_mcp_over_acp")]
112 service: None,
113 runner,
114 }
115 }
116
117 #[cfg(feature = "unstable_mcp_over_acp")]
119 pub fn new_service(
120 name: impl Into<String>,
121 service: impl McpService<Counterpart>,
122 runner: Run,
123 ) -> Self {
124 Self::new_service_with_standalone(service, NoStandalone { name: name.into() }, runner)
125 }
126
127 #[cfg(feature = "unstable_mcp_over_acp")]
132 pub fn new_service_with_standalone(
133 service: impl McpService<Counterpart>,
134 standalone: impl McpServerConnect<Counterpart>,
135 runner: Run,
136 ) -> Self {
137 Self {
138 phantom: PhantomData,
139 connect: Arc::new(standalone),
140 service: Some(Arc::new(service)),
141 runner,
142 }
143 }
144
145 #[cfg(feature = "unstable_mcp_over_acp")]
147 pub(crate) fn into_handler_and_runner(self) -> (McpSessionHandler<Counterpart>, Run)
148 where
149 Counterpart: HasPeer<Agent>,
150 {
151 let Self {
152 phantom: _,
153 connect,
154 service,
155 runner,
156 } = self;
157 let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
158 (
159 McpSessionHandler::new_with_service(server_id, connect, service),
160 runner,
161 )
162 }
163
164 #[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
166 pub(crate) fn into_v2_handler_and_runner(self) -> (V2McpSessionHandler<Counterpart>, Run)
167 where
168 Counterpart: HasPeer<Agent>,
169 {
170 let Self {
171 phantom: _,
172 connect,
173 service,
174 runner,
175 } = self;
176 let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
177 (
178 V2McpSessionHandler::new_with_service(server_id, connect, service),
179 runner,
180 )
181 }
182}
183
184#[cfg(feature = "unstable_mcp_over_acp")]
185struct NoStandalone {
186 name: String,
187}
188
189#[cfg(feature = "unstable_mcp_over_acp")]
190impl<Counterpart: Role> McpServerConnect<Counterpart> for NoStandalone {
191 fn name(&self) -> String {
192 self.name.clone()
193 }
194
195 fn connect(&self, _context: McpConnectionTo<Counterpart>) -> DynConnectTo<role::mcp::Client> {
196 struct Unavailable;
197 impl ConnectTo<role::mcp::Client> for Unavailable {
198 fn connect_to(
199 self,
200 _client: impl ConnectTo<role::mcp::Server>,
201 ) -> impl std::future::Future<Output = Result<(), crate::Error>> + Send {
202 std::future::ready(Err(crate::Error::method_not_found()
203 .data("this MCP service has no standalone transport adapter")))
204 }
205 }
206 DynConnectTo::new(Unavailable)
207 }
208}
209
210#[cfg(feature = "unstable_mcp_over_acp")]
212pub(crate) struct McpSessionHandler<Counterpart: Role>
213where
214 Counterpart: HasPeer<Agent>,
215{
216 server_id: McpServerAcpId,
217 connect: Arc<dyn McpServerConnect<Counterpart>>,
218 active_session: McpActiveSession<Counterpart, V1McpProtocol>,
219}
220
221#[cfg(feature = "unstable_mcp_over_acp")]
222impl<Counterpart: Role> McpSessionHandler<Counterpart>
223where
224 Counterpart: HasPeer<Agent>,
225{
226 fn new_with_service(
227 server_id: McpServerAcpId,
228 connect: Arc<dyn McpServerConnect<Counterpart>>,
229 service: Option<Arc<dyn McpService<Counterpart>>>,
230 ) -> Self {
231 Self {
232 active_session: McpActiveSession::new_with_service(
233 server_id.clone(),
234 connect.clone(),
235 service,
236 ),
237 server_id,
238 connect,
239 }
240 }
241
242 fn append_declaration(&self, mcp_servers: &mut Vec<SchemaMcpServer>) {
244 mcp_servers.push(SchemaMcpServer::Acp(McpServerAcp::new(
245 self.connect.name(),
246 self.server_id.clone(),
247 )));
248 }
249}
250
251#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
253pub(crate) struct V2McpSessionHandler<Counterpart: Role>
254where
255 Counterpart: HasPeer<Agent>,
256{
257 server_id: McpServerAcpId,
258 connect: Arc<dyn McpServerConnect<Counterpart>>,
259 active_session: McpActiveSession<Counterpart, crate::mcp_server::active_session::V2McpProtocol>,
260}
261
262#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
263impl<Counterpart: Role> V2McpSessionHandler<Counterpart>
264where
265 Counterpart: HasPeer<Agent>,
266{
267 #[cfg(test)]
268 fn new(server_id: McpServerAcpId, connect: Arc<dyn McpServerConnect<Counterpart>>) -> Self {
269 Self::new_with_service(server_id, connect, None)
270 }
271
272 fn new_with_service(
273 server_id: McpServerAcpId,
274 connect: Arc<dyn McpServerConnect<Counterpart>>,
275 service: Option<Arc<dyn McpService<Counterpart>>>,
276 ) -> Self {
277 Self {
278 active_session: McpActiveSession::new_with_service(
279 server_id.clone(),
280 connect.clone(),
281 service,
282 ),
283 server_id,
284 connect,
285 }
286 }
287
288 fn declaration(&self) -> crate::schema::v2::McpServer {
289 crate::schema::v2::McpServer::Acp(crate::schema::v2::McpServerAcp::new(
290 self.connect.name(),
291 crate::schema::v2::McpServerAcpId::from(self.server_id.0.clone()),
292 ))
293 }
294
295 fn append_declaration(&self, mcp_servers: &mut Vec<crate::schema::v2::McpServer>) {
296 mcp_servers.push(self.declaration());
297 }
298
299 fn validate_session_setup(request: &UntypedMessage) -> Result<bool, crate::Error> {
300 match request.method() {
301 "session/new" => {
302 crate::schema::v2::NewSessionRequest::parse_message(
303 request.method(),
304 request.params(),
305 )?;
306 Ok(true)
307 }
308 "session/resume" => {
309 crate::schema::v2::ResumeSessionRequest::parse_message(
310 request.method(),
311 request.params(),
312 )?;
313 Ok(true)
314 }
315 #[cfg(feature = "unstable_session_fork")]
316 "session/fork" => {
317 crate::schema::v2::ForkSessionRequest::parse_message(
318 request.method(),
319 request.params(),
320 )?;
321 Ok(true)
322 }
323 _ => Ok(false),
324 }
325 }
326
327 fn append_declaration_to_raw(&self, request: &mut UntypedMessage) -> Result<(), crate::Error> {
328 let serde_json::Value::Object(params) = &mut request.params else {
329 return Err(
330 crate::Error::invalid_params().data("session setup parameters must be an object")
331 );
332 };
333 let declaration =
334 serde_json::to_value(self.declaration()).map_err(crate::Error::into_internal_error)?;
335 match params.get_mut("mcpServers") {
336 Some(serde_json::Value::Array(servers)) => servers.push(declaration),
337 Some(value) => *value = serde_json::Value::Array(vec![declaration]),
338 None => {
339 params.insert(
340 "mcpServers".to_string(),
341 serde_json::Value::Array(vec![declaration]),
342 );
343 }
344 }
345 Ok(())
346 }
347
348 pub fn into_dynamic_handler(
350 self,
351 mcp_servers: &mut Vec<crate::schema::v2::McpServer>,
352 cx: &crate::V2ConnectionTo<Counterpart>,
353 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error> {
354 self.append_declaration(mcp_servers);
355 let cleanup = self.active_session.cleanup();
356 Ok(cx
357 .add_dynamic_handler(self.active_session)?
358 .with_cleanup(cleanup))
359 }
360}
361
362#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
363impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for V2McpSessionHandler<Counterpart>
364where
365 Counterpart: HasPeer<Client> + HasPeer<Agent>,
366{
367 async fn handle_dispatch_from(
368 &mut self,
369 message: Dispatch,
370 cx: ConnectionTo<Counterpart>,
371 ) -> Result<Handled<Dispatch>, crate::Error> {
372 MatchDispatchFrom::new(message, &cx)
373 .if_request_from(Client, async |mut request: UntypedMessage, responder| {
374 if !Self::validate_session_setup(&request)? {
375 return Ok(Handled::No {
376 message: (request, responder),
377 retry: false,
378 });
379 }
380
381 self.append_declaration_to_raw(&mut request)?;
382 Ok(Handled::No {
383 message: (request, responder),
384 retry: false,
385 })
386 })
387 .await
388 .otherwise_delegate(&mut self.active_session)
389 .await
390 }
391
392 fn describe_chain(&self) -> impl std::fmt::Debug {
393 format!("V2McpServer({})", self.connect.name())
394 }
395}
396
397#[cfg(feature = "unstable_mcp_over_acp")]
398impl<Counterpart: Role> McpSessionHandler<Counterpart>
399where
400 Counterpart: HasPeer<Agent>,
401{
402 pub fn into_dynamic_handler(
413 self,
414 request: &mut NewSessionRequest,
415 cx: &ConnectionTo<Counterpart>,
416 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error>
417 where
418 Counterpart: HasPeer<Agent>,
419 {
420 self.append_declaration(&mut request.mcp_servers);
421 let cleanup = self.active_session.cleanup();
422 Ok(cx
423 .add_dynamic_handler(self.active_session)?
424 .with_cleanup(cleanup))
425 }
426}
427
428#[cfg(feature = "unstable_mcp_over_acp")]
429impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for McpSessionHandler<Counterpart>
430where
431 Counterpart: HasPeer<Client> + HasPeer<Agent>,
432{
433 async fn handle_dispatch_from(
434 &mut self,
435 message: Dispatch,
436 cx: ConnectionTo<Counterpart>,
437 ) -> Result<Handled<Dispatch>, crate::Error> {
438 let matcher = MatchDispatchFrom::new(message, &cx)
439 .if_request_from(Client, async |mut request: NewSessionRequest, responder| {
440 self.append_declaration(&mut request.mcp_servers);
441 Ok(Handled::No {
442 message: (request, responder),
443 retry: false,
444 })
445 })
446 .await
447 .if_request_from(
448 Client,
449 async |mut request: LoadSessionRequest, responder| {
450 self.append_declaration(&mut request.mcp_servers);
451 Ok(Handled::No {
452 message: (request, responder),
453 retry: false,
454 })
455 },
456 )
457 .await
458 .if_request_from(
459 Client,
460 async |mut request: ResumeSessionRequest, responder| {
461 self.append_declaration(&mut request.mcp_servers);
462 Ok(Handled::No {
463 message: (request, responder),
464 retry: false,
465 })
466 },
467 )
468 .await;
469
470 #[cfg(feature = "unstable_session_fork")]
471 let matcher = matcher
472 .if_request_from(
473 Client,
474 async |mut request: ForkSessionRequest, responder| {
475 self.append_declaration(&mut request.mcp_servers);
476 Ok(Handled::No {
477 message: (request, responder),
478 retry: false,
479 })
480 },
481 )
482 .await;
483
484 matcher.otherwise_delegate(&mut self.active_session).await
485 }
486
487 fn describe_chain(&self) -> impl std::fmt::Debug {
488 format!("McpServer({})", self.connect.name())
489 }
490}
491
492impl<Run> ConnectTo<role::mcp::Client> for McpServer<role::mcp::Client, Run>
493where
494 Run: RunWithConnectionTo<role::mcp::Client> + 'static,
495{
496 async fn connect_to(
497 self,
498 client: impl ConnectTo<role::mcp::Server>,
499 ) -> Result<(), crate::Error> {
500 let Self {
501 connect,
502 runner,
503 phantom: _,
504 #[cfg(feature = "unstable_mcp_over_acp")]
505 service: _,
506 } = self;
507
508 let (tx, mut rx) = mpsc::unbounded();
509
510 role::mcp::Server
511 .builder()
512 .with_runner(runner)
513 .on_receive_dispatch(
514 async |message_from_client: Dispatch, _cx| {
515 tx.unbounded_send(message_from_client)
516 .map_err(|_| crate::util::internal_error("nobody listening to mcp server"))
517 },
518 crate::on_receive_dispatch!(),
519 )
520 .with_spawned(async move |connection_to_client| {
521 let spawned_server: DynConnectTo<role::mcp::Client> =
522 connect.connect(McpConnectionTo {
523 context: McpConnectionContext::Standalone,
524 connection: connection_to_client.clone(),
525 #[cfg(feature = "unstable_mcp_over_acp")]
526 cleanup: None,
527 });
528
529 role::mcp::Client
530 .builder()
531 .on_receive_dispatch(
532 async |message_from_server: Dispatch, _| {
533 connection_to_client.send_proxied_message(message_from_server)
535 },
536 crate::on_receive_dispatch!(),
537 )
538 .connect_with(spawned_server, async |connection_to_server| {
539 while let Some(message_from_client) = rx.next().await {
540 connection_to_server.send_proxied_message(message_from_client)?;
541 }
542 Ok(())
543 })
544 .await
545 })
546 .connect_to(client)
547 .await
548 }
549}
550
551#[cfg(all(
552 test,
553 feature = "unstable_mcp_over_acp",
554 feature = "unstable_protocol_v2"
555))]
556mod tests {
557 use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
558
559 use serde::Serialize;
560 use serde_json::{Value, json};
561
562 use super::V2McpSessionHandler;
563 use crate::{
564 Conductor, DynConnectTo, Error, UntypedMessage,
565 mcp_server::{McpConnectionTo, McpServerConnect},
566 role,
567 schema::{
568 v1::McpServerAcpId,
569 v2::{self, McpServer},
570 },
571 };
572
573 struct UnusedMcpConnect;
574
575 impl McpServerConnect<Conductor> for UnusedMcpConnect {
576 fn name(&self) -> String {
577 "global-v2-server".to_owned()
578 }
579
580 fn connect(&self, _context: McpConnectionTo<Conductor>) -> DynConnectTo<role::mcp::Client> {
581 panic!("declaration tests must not connect to the MCP server")
582 }
583 }
584
585 fn handler() -> V2McpSessionHandler<Conductor> {
586 V2McpSessionHandler::new(
587 McpServerAcpId::new("global-v2-server-id"),
588 Arc::new(UnusedMcpConnect),
589 )
590 }
591
592 fn existing_server() -> McpServer {
593 McpServer::Other(v2::OtherMcpServer::new(
594 "_future_transport",
595 BTreeMap::from([("futureOption".to_owned(), json!({ "nested": true }))]),
596 ))
597 }
598
599 fn meta() -> v2::Meta {
600 v2::Meta::from_iter([("preserved".to_owned(), json!({ "nested": true }))])
601 }
602
603 fn assert_raw_append_preserves_params(
604 handler: &V2McpSessionHandler<Conductor>,
605 method: &str,
606 params: impl Serialize,
607 ) -> Result<v2::McpServerAcpId, Error> {
608 let mut params = serde_json::to_value(params)?;
609 let Value::Object(params_object) = &mut params else {
610 panic!("session setup params should serialize as an object");
611 };
612 params_object.insert(
613 "_futureSessionField".to_owned(),
614 json!({ "must": ["remain", "untouched"] }),
615 );
616
617 let mut expected = params.clone();
618 expected
619 .get_mut("mcpServers")
620 .and_then(Value::as_array_mut)
621 .expect("test request should contain mcpServers")
622 .push(serde_json::to_value(handler.declaration())?);
623
624 let mut request = UntypedMessage::new(method, params)?;
625 assert!(V2McpSessionHandler::<Conductor>::validate_session_setup(
626 &request
627 )?);
628 handler.append_declaration_to_raw(&mut request)?;
629
630 assert_eq!(
631 request.params, expected,
632 "global attachment must only append its declaration"
633 );
634
635 let appended = request
636 .params
637 .get("mcpServers")
638 .and_then(Value::as_array)
639 .and_then(|servers| servers.last())
640 .cloned()
641 .expect("global declaration should be appended");
642 match serde_json::from_value::<McpServer>(appended)? {
643 McpServer::Acp(server) => {
644 assert_eq!(server.name, "global-v2-server");
645 Ok(server.server_id)
646 }
647 server => panic!("expected an ACP server declaration, got {server:?}"),
648 }
649 }
650
651 #[test]
652 fn v2_global_mcp_declaration_preserves_all_session_setup_params() -> Result<(), Error> {
653 let handler = handler();
654 let cwd = PathBuf::from("/tmp/global-v2-mcp");
655 let additional_directory = PathBuf::from("/tmp/global-v2-mcp-additional");
656 let session_id = v2::SessionId::new("session-to-resume");
657 let existing_server = existing_server();
658
659 let new_server_id = assert_raw_append_preserves_params(
660 &handler,
661 "session/new",
662 v2::NewSessionRequest::new(cwd.clone())
663 .additional_directories([additional_directory.clone()])
664 .mcp_servers(vec![existing_server.clone()])
665 .meta(meta()),
666 )?;
667
668 let resume_server_id = assert_raw_append_preserves_params(
669 &handler,
670 "session/resume",
671 v2::ResumeSessionRequest::new(session_id.clone(), cwd.clone())
672 .additional_directories([additional_directory.clone()])
673 .mcp_servers(vec![existing_server.clone()])
674 .replay_from(v2::ReplayFrom::Start(
675 v2::ReplayFromStart::new().meta(meta()),
676 ))
677 .meta(meta()),
678 )?;
679 assert_eq!(resume_server_id, new_server_id);
680
681 #[cfg(feature = "unstable_session_fork")]
682 {
683 let fork_server_id = assert_raw_append_preserves_params(
684 &handler,
685 "session/fork",
686 v2::ForkSessionRequest::new(session_id, cwd)
687 .additional_directories([additional_directory])
688 .mcp_servers(vec![existing_server])
689 .meta(meta()),
690 )?;
691 assert_eq!(fork_server_id, new_server_id);
692 }
693
694 Ok(())
695 }
696
697 #[test]
698 fn v2_global_mcp_handler_ignores_non_setup_requests() -> Result<(), Error> {
699 let request = UntypedMessage::new(
700 "session/prompt",
701 json!({
702 "sessionId": "session-to-prompt",
703 "prompt": []
704 }),
705 )?;
706
707 assert!(!V2McpSessionHandler::<Conductor>::validate_session_setup(
708 &request
709 )?);
710 assert_eq!(request.method(), "session/prompt");
711 Ok(())
712 }
713}