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 util::MatchDispatchFrom,
25 v1::schema::{
26 LoadSessionRequest, McpServer as SchemaMcpServer, McpServerAcp, McpServerAcpId,
27 NewSessionRequest, ResumeSessionRequest,
28 },
29};
30
31#[cfg(feature = "unstable_mcp_over_acp")]
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::v1::schema::ForkSessionRequest;
39
40#[cfg(feature = "unstable_mcp_over_acp")]
43fn append_raw_declaration(
44 request: &mut UntypedMessage,
45 declaration: &impl serde::Serialize,
46) -> Result<(), crate::Error> {
47 let serde_json::Value::Object(params) = &mut request.params else {
48 return Err(
49 crate::Error::invalid_params().data("session setup parameters must be an object")
50 );
51 };
52 let declaration =
53 serde_json::to_value(declaration).map_err(crate::Error::into_internal_error)?;
54 match params.get_mut("mcpServers") {
55 Some(serde_json::Value::Array(servers)) => servers.push(declaration),
56 Some(value) => *value = serde_json::Value::Array(vec![declaration]),
57 None => {
58 params.insert(
59 "mcpServers".to_string(),
60 serde_json::Value::Array(vec![declaration]),
61 );
62 }
63 }
64 Ok(())
65}
66
67pub struct McpServer<Counterpart: Role, Run = NullRun> {
93 phantom: PhantomData<Counterpart>,
95
96 connect: Arc<dyn McpServerConnect<Counterpart>>,
98
99 #[cfg(feature = "unstable_mcp_over_acp")]
100 service: Option<Arc<dyn McpService<Counterpart>>>,
101
102 runner: Run,
111}
112
113impl<Counterpart: Role + std::fmt::Debug, Run: std::fmt::Debug> std::fmt::Debug
114 for McpServer<Counterpart, Run>
115{
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_struct("McpServer")
118 .field("phantom", &self.phantom)
119 .field("runner", &self.runner)
120 .finish_non_exhaustive()
121 }
122}
123
124impl<Counterpart: Role, Run> McpServer<Counterpart, Run>
125where
126 Run: RunWithConnectionTo<Counterpart>,
127{
128 pub fn new(c: impl McpServerConnect<Counterpart>, runner: Run) -> Self {
135 McpServer {
136 phantom: PhantomData,
137 connect: Arc::new(c),
138 #[cfg(feature = "unstable_mcp_over_acp")]
139 service: None,
140 runner,
141 }
142 }
143
144 #[cfg(feature = "unstable_mcp_over_acp")]
146 pub fn new_service(
147 name: impl Into<String>,
148 service: impl McpService<Counterpart>,
149 runner: Run,
150 ) -> Self {
151 Self::new_service_with_standalone(service, NoStandalone { name: name.into() }, runner)
152 }
153
154 #[cfg(feature = "unstable_mcp_over_acp")]
159 pub fn new_service_with_standalone(
160 service: impl McpService<Counterpart>,
161 standalone: impl McpServerConnect<Counterpart>,
162 runner: Run,
163 ) -> Self {
164 Self {
165 phantom: PhantomData,
166 connect: Arc::new(standalone),
167 service: Some(Arc::new(service)),
168 runner,
169 }
170 }
171
172 #[cfg(feature = "unstable_mcp_over_acp")]
174 pub(crate) fn into_handler_and_runner(self) -> (McpSessionHandler<Counterpart>, Run)
175 where
176 Counterpart: HasPeer<Agent>,
177 {
178 let Self {
179 phantom: _,
180 connect,
181 service,
182 runner,
183 } = self;
184 let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
185 (
186 McpSessionHandler::new_with_service(server_id, connect, service),
187 runner,
188 )
189 }
190
191 #[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
193 pub(crate) fn into_v2_handler_and_runner(self) -> (V2McpSessionHandler<Counterpart>, Run)
194 where
195 Counterpart: HasPeer<Agent>,
196 {
197 let Self {
198 phantom: _,
199 connect,
200 service,
201 runner,
202 } = self;
203 let server_id = McpServerAcpId::new(format!("mcp-server:{}", Uuid::new_v4()));
204 (
205 V2McpSessionHandler::new_with_service(server_id, connect, service),
206 runner,
207 )
208 }
209}
210
211#[cfg(feature = "unstable_mcp_over_acp")]
212struct NoStandalone {
213 name: String,
214}
215
216#[cfg(feature = "unstable_mcp_over_acp")]
217impl<Counterpart: Role> McpServerConnect<Counterpart> for NoStandalone {
218 fn name(&self) -> String {
219 self.name.clone()
220 }
221
222 fn connect(&self, _context: McpConnectionTo<Counterpart>) -> DynConnectTo<role::mcp::Client> {
223 struct Unavailable;
224 impl ConnectTo<role::mcp::Client> for Unavailable {
225 fn connect_to(
226 self,
227 _client: impl ConnectTo<role::mcp::Server>,
228 ) -> impl std::future::Future<Output = Result<(), crate::Error>> + Send {
229 std::future::ready(Err(crate::Error::method_not_found()
230 .data("this MCP service has no standalone transport adapter")))
231 }
232 }
233 DynConnectTo::new(Unavailable)
234 }
235}
236
237#[cfg(feature = "unstable_mcp_over_acp")]
239pub(crate) struct McpSessionHandler<Counterpart: Role>
240where
241 Counterpart: HasPeer<Agent>,
242{
243 server_id: McpServerAcpId,
244 connect: Arc<dyn McpServerConnect<Counterpart>>,
245 active_session: McpActiveSession<Counterpart, V1McpProtocol>,
246}
247
248#[cfg(feature = "unstable_mcp_over_acp")]
249impl<Counterpart: Role> McpSessionHandler<Counterpart>
250where
251 Counterpart: HasPeer<Agent>,
252{
253 fn new_with_service(
254 server_id: McpServerAcpId,
255 connect: Arc<dyn McpServerConnect<Counterpart>>,
256 service: Option<Arc<dyn McpService<Counterpart>>>,
257 ) -> Self {
258 Self {
259 active_session: McpActiveSession::new_with_service(
260 server_id.clone(),
261 connect.clone(),
262 service,
263 ),
264 server_id,
265 connect,
266 }
267 }
268
269 fn declaration(&self) -> SchemaMcpServer {
270 SchemaMcpServer::Acp(McpServerAcp::new(
271 self.connect.name(),
272 self.server_id.clone(),
273 ))
274 }
275
276 fn append_declaration(&self, mcp_servers: &mut Vec<SchemaMcpServer>) {
278 mcp_servers.push(self.declaration());
279 }
280
281 fn validate_session_setup(request: &UntypedMessage) -> Result<bool, crate::Error> {
284 match request.method() {
285 "session/new" => {
286 NewSessionRequest::parse_message(request.method(), request.params())?;
287 Ok(true)
288 }
289 "session/load" => {
290 LoadSessionRequest::parse_message(request.method(), request.params())?;
291 Ok(true)
292 }
293 "session/resume" => {
294 ResumeSessionRequest::parse_message(request.method(), request.params())?;
295 Ok(true)
296 }
297 #[cfg(feature = "unstable_session_fork")]
298 "session/fork" => {
299 ForkSessionRequest::parse_message(request.method(), request.params())?;
300 Ok(true)
301 }
302 _ => Ok(false),
303 }
304 }
305}
306
307#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
309pub(crate) struct V2McpSessionHandler<Counterpart: Role>
310where
311 Counterpart: HasPeer<Agent>,
312{
313 server_id: McpServerAcpId,
314 connect: Arc<dyn McpServerConnect<Counterpart>>,
315 active_session: McpActiveSession<Counterpart, crate::mcp_server::active_session::V2McpProtocol>,
316}
317
318#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
319impl<Counterpart: Role> V2McpSessionHandler<Counterpart>
320where
321 Counterpart: HasPeer<Agent>,
322{
323 #[cfg(test)]
324 fn new(server_id: McpServerAcpId, connect: Arc<dyn McpServerConnect<Counterpart>>) -> Self {
325 Self::new_with_service(server_id, connect, None)
326 }
327
328 fn new_with_service(
329 server_id: McpServerAcpId,
330 connect: Arc<dyn McpServerConnect<Counterpart>>,
331 service: Option<Arc<dyn McpService<Counterpart>>>,
332 ) -> Self {
333 Self {
334 active_session: McpActiveSession::new_with_service(
335 server_id.clone(),
336 connect.clone(),
337 service,
338 ),
339 server_id,
340 connect,
341 }
342 }
343
344 fn declaration(&self) -> crate::v2::schema::McpServer {
345 crate::v2::schema::McpServer::Acp(crate::v2::schema::McpServerAcp::new(
346 self.connect.name(),
347 crate::v2::schema::McpServerAcpId::from(self.server_id.0.clone()),
348 ))
349 }
350
351 fn append_declaration(&self, mcp_servers: &mut Vec<crate::v2::schema::McpServer>) {
352 mcp_servers.push(self.declaration());
353 }
354
355 fn validate_session_setup(request: &UntypedMessage) -> Result<bool, crate::Error> {
356 match request.method() {
357 "session/new" => {
358 crate::v2::schema::NewSessionRequest::parse_message(
359 request.method(),
360 request.params(),
361 )?;
362 Ok(true)
363 }
364 "session/resume" => {
365 crate::v2::schema::ResumeSessionRequest::parse_message(
366 request.method(),
367 request.params(),
368 )?;
369 Ok(true)
370 }
371 #[cfg(feature = "unstable_session_fork")]
372 "session/fork" => {
373 crate::v2::schema::ForkSessionRequest::parse_message(
374 request.method(),
375 request.params(),
376 )?;
377 Ok(true)
378 }
379 _ => Ok(false),
380 }
381 }
382
383 fn append_declaration_to_raw(&self, request: &mut UntypedMessage) -> Result<(), crate::Error> {
389 let serde_json::Value::Object(params) = &mut request.params else {
390 return Err(
391 crate::Error::invalid_params().data("session setup parameters must be an object")
392 );
393 };
394 if let Some(serde_json::Value::Array(servers)) = params.get("mcpServers") {
395 let name = self.connect.name();
396 if servers.iter().any(|server| {
397 server.get("name").and_then(serde_json::Value::as_str) == Some(name.as_str())
398 }) {
399 return Err(crate::Error::invalid_params().data(format!(
400 "MCP server `{name}` is configured by the proxy, so mcpServers cannot \
401 declare another server with that name; names must be unique within mcpServers"
402 )));
403 }
404 }
405 append_raw_declaration(request, &self.declaration())
406 }
407
408 pub fn into_dynamic_handler(
415 self,
416 mcp_servers: &mut Vec<crate::v2::schema::McpServer>,
417 cx: &crate::v2::ConnectionTo<Counterpart>,
418 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error> {
419 let name = self.connect.name();
420 if mcp_servers
421 .iter()
422 .any(|server| v2_mcp_server_name(server) == Some(name.as_str()))
423 {
424 return Err(crate::Error::invalid_params().data(format!(
425 "MCP server name `{name}` is already declared; names must be unique within mcpServers"
426 )));
427 }
428 self.append_declaration(mcp_servers);
429 let cleanup = self.active_session.cleanup();
430 Ok(cx
431 .add_dynamic_handler(self.active_session)?
432 .with_cleanup(cleanup))
433 }
434}
435
436#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
438fn v2_mcp_server_name(server: &crate::v2::schema::McpServer) -> Option<&str> {
439 use crate::v2::schema::McpServer;
440 match server {
441 McpServer::Http(server) => Some(&server.name),
442 McpServer::Acp(server) => Some(&server.name),
443 McpServer::Stdio(server) => Some(&server.name),
444 McpServer::Other(server) => server
445 .fields
446 .get("name")
447 .and_then(serde_json::Value::as_str),
448 _ => None,
449 }
450}
451
452#[cfg(all(feature = "unstable_mcp_over_acp", feature = "unstable_protocol_v2"))]
453impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for V2McpSessionHandler<Counterpart>
454where
455 Counterpart: HasPeer<Client> + HasPeer<Agent>,
456{
457 async fn handle_dispatch_from(
458 &mut self,
459 message: Dispatch,
460 cx: ConnectionTo<Counterpart>,
461 ) -> Result<Handled<Dispatch>, crate::Error> {
462 MatchDispatchFrom::new(message, &cx)
463 .if_request_from(Client, async |mut request: UntypedMessage, responder| {
464 if !Self::validate_session_setup(&request)? {
465 return Ok(Handled::No {
466 message: (request, responder),
467 retry: false,
468 });
469 }
470
471 if let Err(error) = self.append_declaration_to_raw(&mut request) {
472 responder.respond_with_error(error)?;
475 return Ok(Handled::Yes);
476 }
477 Ok(Handled::No {
478 message: (request, responder),
479 retry: false,
480 })
481 })
482 .await
483 .otherwise_delegate(&mut self.active_session)
484 .await
485 }
486
487 fn describe_chain(&self) -> impl std::fmt::Debug {
488 format!("V2McpServer({})", self.connect.name())
489 }
490}
491
492#[cfg(feature = "unstable_mcp_over_acp")]
493impl<Counterpart: Role> McpSessionHandler<Counterpart>
494where
495 Counterpart: HasPeer<Agent>,
496{
497 pub fn into_dynamic_handler(
508 self,
509 request: &mut NewSessionRequest,
510 cx: &ConnectionTo<Counterpart>,
511 ) -> Result<DynamicHandlerGuard<Counterpart>, crate::Error>
512 where
513 Counterpart: HasPeer<Agent>,
514 {
515 self.append_declaration(&mut request.mcp_servers);
516 let cleanup = self.active_session.cleanup();
517 Ok(cx
518 .add_dynamic_handler(self.active_session)?
519 .with_cleanup(cleanup))
520 }
521}
522
523#[cfg(feature = "unstable_mcp_over_acp")]
524impl<Counterpart: Role> HandleDispatchFrom<Counterpart> for McpSessionHandler<Counterpart>
525where
526 Counterpart: HasPeer<Client> + HasPeer<Agent>,
527{
528 async fn handle_dispatch_from(
529 &mut self,
530 message: Dispatch,
531 cx: ConnectionTo<Counterpart>,
532 ) -> Result<Handled<Dispatch>, crate::Error> {
533 MatchDispatchFrom::new(message, &cx)
536 .if_request_from(Client, async |mut request: UntypedMessage, responder| {
537 if Self::validate_session_setup(&request)? {
538 append_raw_declaration(&mut request, &self.declaration())?;
539 }
540 Ok(Handled::No {
541 message: (request, responder),
542 retry: false,
543 })
544 })
545 .await
546 .otherwise_delegate(&mut self.active_session)
547 .await
548 }
549
550 fn describe_chain(&self) -> impl std::fmt::Debug {
551 format!("McpServer({})", self.connect.name())
552 }
553}
554
555impl<Run> ConnectTo<role::mcp::Client> for McpServer<role::mcp::Client, Run>
556where
557 Run: RunWithConnectionTo<role::mcp::Client> + 'static,
558{
559 async fn connect_to(
560 self,
561 client: impl ConnectTo<role::mcp::Server>,
562 ) -> Result<(), crate::Error> {
563 let Self {
564 connect,
565 runner,
566 phantom: _,
567 #[cfg(feature = "unstable_mcp_over_acp")]
568 service: _,
569 } = self;
570
571 let (tx, mut rx) = mpsc::unbounded();
572
573 role::mcp::Server
574 .builder()
575 .with_runner(runner)
576 .on_receive_dispatch(
577 async |message_from_client: Dispatch, _cx| {
578 tx.unbounded_send(message_from_client)
579 .map_err(|_| crate::util::internal_error("nobody listening to mcp server"))
580 },
581 crate::on_receive_dispatch!(),
582 )
583 .with_spawned(async move |connection_to_client| {
584 let spawned_server: DynConnectTo<role::mcp::Client> =
585 connect.connect(McpConnectionTo {
586 context: McpConnectionContext::Standalone,
587 connection: connection_to_client.clone(),
588 #[cfg(feature = "unstable_mcp_over_acp")]
589 cleanup: None,
590 });
591
592 role::mcp::Client
593 .builder()
594 .on_receive_dispatch(
595 async |message_from_server: Dispatch, _| {
596 connection_to_client.send_proxied_message(message_from_server)
598 },
599 crate::on_receive_dispatch!(),
600 )
601 .connect_with(spawned_server, async |connection_to_server| {
602 while let Some(message_from_client) = rx.next().await {
603 connection_to_server.send_proxied_message(message_from_client)?;
604 }
605 Ok(())
606 })
607 .await
608 })
609 .connect_to(client)
610 .await
611 }
612}
613
614#[cfg(all(
615 test,
616 feature = "unstable_mcp_over_acp",
617 feature = "unstable_protocol_v2"
618))]
619mod tests {
620 use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
621
622 use serde::Serialize;
623 use serde_json::{Value, json};
624
625 use super::V2McpSessionHandler;
626 use crate::{
627 Conductor, DynConnectTo, Error, UntypedMessage,
628 mcp_server::{McpConnectionTo, McpServerConnect},
629 role,
630 v1::schema::McpServerAcpId,
631 v2::schema::{self as v2, McpServer},
632 };
633
634 struct UnusedMcpConnect;
635
636 impl McpServerConnect<Conductor> for UnusedMcpConnect {
637 fn name(&self) -> String {
638 "global-v2-server".to_owned()
639 }
640
641 fn connect(&self, _context: McpConnectionTo<Conductor>) -> DynConnectTo<role::mcp::Client> {
642 panic!("declaration tests must not connect to the MCP server")
643 }
644 }
645
646 fn handler() -> V2McpSessionHandler<Conductor> {
647 V2McpSessionHandler::new(
648 McpServerAcpId::new("global-v2-server-id"),
649 Arc::new(UnusedMcpConnect),
650 )
651 }
652
653 fn existing_server() -> McpServer {
654 McpServer::Other(v2::OtherMcpServer::new(
655 "_future_transport",
656 BTreeMap::from([("futureOption".to_owned(), json!({ "nested": true }))]),
657 ))
658 }
659
660 fn meta() -> v2::Meta {
661 v2::Meta::from_iter([("preserved".to_owned(), json!({ "nested": true }))])
662 }
663
664 fn assert_raw_append_preserves_params(
665 handler: &V2McpSessionHandler<Conductor>,
666 method: &str,
667 params: impl Serialize,
668 ) -> Result<v2::McpServerAcpId, Error> {
669 let mut params = serde_json::to_value(params)?;
670 let Value::Object(params_object) = &mut params else {
671 panic!("session setup params should serialize as an object");
672 };
673 params_object.insert(
674 "_futureSessionField".to_owned(),
675 json!({ "must": ["remain", "untouched"] }),
676 );
677
678 let mut expected = params.clone();
679 expected
680 .get_mut("mcpServers")
681 .and_then(Value::as_array_mut)
682 .expect("test request should contain mcpServers")
683 .push(serde_json::to_value(handler.declaration())?);
684
685 let mut request = UntypedMessage::new(method, params)?;
686 assert!(V2McpSessionHandler::<Conductor>::validate_session_setup(
687 &request
688 )?);
689 handler.append_declaration_to_raw(&mut request)?;
690
691 assert_eq!(
692 request.params, expected,
693 "global attachment must only append its declaration"
694 );
695
696 let appended = request
697 .params
698 .get("mcpServers")
699 .and_then(Value::as_array)
700 .and_then(|servers| servers.last())
701 .cloned()
702 .expect("global declaration should be appended");
703 match serde_json::from_value::<McpServer>(appended)? {
704 McpServer::Acp(server) => {
705 assert_eq!(server.name, "global-v2-server");
706 Ok(server.server_id)
707 }
708 server => panic!("expected an ACP server declaration, got {server:?}"),
709 }
710 }
711
712 #[test]
713 fn v2_global_mcp_declaration_preserves_all_session_setup_params() -> Result<(), Error> {
714 let handler = handler();
715 let cwd = PathBuf::from("/tmp/global-v2-mcp");
716 let additional_directory = PathBuf::from("/tmp/global-v2-mcp-additional");
717 let session_id = v2::SessionId::new("session-to-resume");
718 let existing_server = existing_server();
719
720 let new_server_id = assert_raw_append_preserves_params(
721 &handler,
722 "session/new",
723 v2::NewSessionRequest::new(cwd.clone())
724 .additional_directories([additional_directory.clone()])
725 .mcp_servers(vec![existing_server.clone()])
726 .meta(meta()),
727 )?;
728
729 let resume_server_id = assert_raw_append_preserves_params(
730 &handler,
731 "session/resume",
732 v2::ResumeSessionRequest::new(session_id.clone(), cwd.clone())
733 .additional_directories([additional_directory.clone()])
734 .mcp_servers(vec![existing_server.clone()])
735 .replay_from(v2::ReplayFrom::Start(
736 v2::ReplayFromStart::new().meta(meta()),
737 ))
738 .meta(meta()),
739 )?;
740 assert_eq!(resume_server_id, new_server_id);
741
742 #[cfg(feature = "unstable_session_fork")]
743 {
744 let fork_server_id = assert_raw_append_preserves_params(
745 &handler,
746 "session/fork",
747 v2::ForkSessionRequest::new(session_id, cwd)
748 .additional_directories([additional_directory])
749 .mcp_servers(vec![existing_server])
750 .meta(meta()),
751 )?;
752 assert_eq!(fork_server_id, new_server_id);
753 }
754
755 Ok(())
756 }
757
758 fn assert_raw_append_rejects_name_collision(
759 handler: &V2McpSessionHandler<Conductor>,
760 method: &str,
761 params: impl Serialize,
762 ) -> Result<(), Error> {
763 let params = serde_json::to_value(params)?;
764 let mut request = UntypedMessage::new(method, params.clone())?;
765 assert!(V2McpSessionHandler::<Conductor>::validate_session_setup(
766 &request
767 )?);
768
769 let error = handler
770 .append_declaration_to_raw(&mut request)
771 .expect_err("a colliding MCP server name must be rejected");
772 assert_eq!(error.code, crate::ErrorCode::INVALID_PARAMS);
773 let message = error
774 .data
775 .as_ref()
776 .and_then(Value::as_str)
777 .expect("the error should explain the collision");
778 assert!(
779 message.contains("`global-v2-server`") && message.contains("configured by the proxy"),
780 "unexpected error message: {message}"
781 );
782 assert_eq!(
783 request.params, params,
784 "a rejected request must not be modified"
785 );
786 Ok(())
787 }
788
789 #[test]
790 fn v2_global_mcp_declaration_rejects_client_server_with_same_name() -> Result<(), Error> {
791 let handler = handler();
792 let cwd = PathBuf::from("/tmp/global-v2-mcp");
793 let session_id = v2::SessionId::new("session-to-resume");
794 let servers = vec![
795 existing_server(),
796 McpServer::Http(v2::McpServerHttp::new(
797 "global-v2-server",
798 "https://example.com/mcp",
799 )),
800 ];
801
802 assert_raw_append_rejects_name_collision(
803 &handler,
804 "session/new",
805 v2::NewSessionRequest::new(cwd.clone()).mcp_servers(servers.clone()),
806 )?;
807 assert_raw_append_rejects_name_collision(
808 &handler,
809 "session/resume",
810 v2::ResumeSessionRequest::new(session_id.clone(), cwd.clone())
811 .mcp_servers(servers.clone()),
812 )?;
813 #[cfg(feature = "unstable_session_fork")]
814 assert_raw_append_rejects_name_collision(
815 &handler,
816 "session/fork",
817 v2::ForkSessionRequest::new(session_id, cwd).mcp_servers(servers),
818 )?;
819
820 Ok(())
821 }
822
823 #[test]
824 fn v2_global_mcp_declaration_accepts_client_servers_with_other_names() -> Result<(), Error> {
825 let handler = handler();
826 let server_id = assert_raw_append_preserves_params(
827 &handler,
828 "session/new",
829 v2::NewSessionRequest::new(PathBuf::from("/tmp/global-v2-mcp")).mcp_servers(vec![
830 McpServer::Http(v2::McpServerHttp::new(
831 "client-server",
832 "https://example.com/mcp",
833 )),
834 ]),
835 )?;
836 assert_eq!(server_id.to_string(), "global-v2-server-id");
837 Ok(())
838 }
839
840 #[test]
841 fn v2_global_mcp_handler_ignores_non_setup_requests() -> Result<(), Error> {
842 let request = UntypedMessage::new(
843 "session/prompt",
844 json!({
845 "sessionId": "session-to-prompt",
846 "prompt": []
847 }),
848 )?;
849
850 assert!(!V2McpSessionHandler::<Conductor>::validate_session_setup(
851 &request
852 )?);
853 assert_eq!(request.method(), "session/prompt");
854 Ok(())
855 }
856}
857
858#[cfg(all(test, feature = "unstable_mcp_over_acp"))]
859mod v1_tests {
860 use std::{path::PathBuf, sync::Arc};
861
862 use serde::Serialize;
863 use serde_json::{Value, json};
864
865 use super::{McpSessionHandler, append_raw_declaration};
866 use crate::{
867 Conductor, DynConnectTo, Error, UntypedMessage,
868 mcp_server::{McpConnectionTo, McpServerConnect},
869 role,
870 v1::schema::{
871 LoadSessionRequest, McpServer, McpServerAcpId, McpServerHttp, NewSessionRequest,
872 ResumeSessionRequest, SessionId,
873 },
874 };
875
876 struct UnusedMcpConnect;
877
878 impl McpServerConnect<Conductor> for UnusedMcpConnect {
879 fn name(&self) -> String {
880 "global-v1-server".to_owned()
881 }
882
883 fn connect(&self, _context: McpConnectionTo<Conductor>) -> DynConnectTo<role::mcp::Client> {
884 panic!("declaration tests must not connect to the MCP server")
885 }
886 }
887
888 fn handler() -> McpSessionHandler<Conductor> {
889 McpSessionHandler::new_with_service(
890 McpServerAcpId::new("global-v1-server-id"),
891 Arc::new(UnusedMcpConnect),
892 None,
893 )
894 }
895
896 fn assert_raw_append_preserves_params(
897 handler: &McpSessionHandler<Conductor>,
898 method: &str,
899 params: impl Serialize,
900 ) -> Result<(), Error> {
901 let mut params = serde_json::to_value(params)?;
902 let Value::Object(params_object) = &mut params else {
903 panic!("session setup params should serialize as an object");
904 };
905 params_object.insert("futureField".to_owned(), json!("kept"));
907 params_object.insert(
908 "_futureSessionField".to_owned(),
909 json!({ "must": ["remain", "untouched"] }),
910 );
911
912 let mut expected = params.clone();
913 expected
914 .get_mut("mcpServers")
915 .and_then(Value::as_array_mut)
916 .expect("test request should contain mcpServers")
917 .push(serde_json::to_value(handler.declaration())?);
918
919 let mut request = UntypedMessage::new(method, params)?;
920 assert!(McpSessionHandler::<Conductor>::validate_session_setup(
921 &request
922 )?);
923 append_raw_declaration(&mut request, &handler.declaration())?;
924
925 assert_eq!(
926 request.params, expected,
927 "global attachment must only append its declaration"
928 );
929 Ok(())
930 }
931
932 #[test]
933 fn v1_global_mcp_declaration_preserves_all_session_setup_params() -> Result<(), Error> {
934 let handler = handler();
935 let cwd = PathBuf::from("/tmp/global-v1-mcp");
936 let session_id = SessionId::new("session-to-restore");
937 let existing = vec![McpServer::Http(McpServerHttp::new(
938 "client-server",
939 "https://example.com/mcp",
940 ))];
941
942 assert_raw_append_preserves_params(
943 &handler,
944 "session/new",
945 NewSessionRequest::new(cwd.clone()).mcp_servers(existing.clone()),
946 )?;
947 assert_raw_append_preserves_params(
948 &handler,
949 "session/load",
950 LoadSessionRequest::new(session_id.clone(), cwd.clone()).mcp_servers(existing.clone()),
951 )?;
952 assert_raw_append_preserves_params(
953 &handler,
954 "session/resume",
955 ResumeSessionRequest::new(session_id, cwd).mcp_servers(existing),
956 )?;
957 Ok(())
958 }
959
960 #[test]
961 fn v1_global_mcp_declaration_ignores_other_requests() -> Result<(), Error> {
962 let request = UntypedMessage::new("session/prompt", json!({ "sessionId": "s" }))?;
963 assert!(!McpSessionHandler::<Conductor>::validate_session_setup(
964 &request
965 )?);
966 Ok(())
967 }
968}