Skip to main content

agent_client_protocol/mcp_server/
server.rs

1//! MCP server construction, direct serving, and optional ACP attachment.
2
3use 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/// Append an MCP server declaration to the `mcpServers` array of a raw
41/// session setup request, leaving every other field as received.
42#[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
67/// A runtime-agnostic MCP server.
68///
69/// `McpServer` wraps an [`McpServerConnect`](`super::McpServerConnect`)
70/// implementation. A server whose counterpart is [`role::mcp::Client`] can be
71/// connected directly as a standalone MCP component. With the
72/// `unstable_mcp_over_acp` feature, servers can instead be attached to ACP
73/// session setup through `Builder::with_mcp_server` or
74/// `SessionBuilder::with_mcp_server`. When `unstable_protocol_v2` is also
75/// enabled, `Proxy.v2().with_mcp_server` attaches a server globally to draft
76/// v2 setup requests. `v2::SessionBuilder::with_mcp_server` attaches one to a
77/// single new v2 session, while `v2::ResumeSessionBuilder::with_mcp_server`
78/// attaches one to a single resumed session. With `unstable_session_fork`,
79/// `v2::ForkSessionBuilder::with_mcp_server` attaches one to a single forked
80/// session.
81///
82/// # Creating an MCP Server
83///
84/// The `agent-client-protocol-rmcp` crate provides builder APIs for MCP tools
85/// backed by the `rmcp` crate.
86///
87/// Or implement [`McpServerConnect`](`super::McpServerConnect`) for custom server behavior:
88///
89/// ```rust,ignore
90/// let server = McpServer::new(MyCustomServerConnect, NullRun);
91/// ```
92pub struct McpServer<Counterpart: Role, Run = NullRun> {
93    /// The host role that is serving up this MCP server
94    phantom: PhantomData<Counterpart>,
95
96    /// The "connect" instance
97    connect: Arc<dyn McpServerConnect<Counterpart>>,
98
99    #[cfg(feature = "unstable_mcp_over_acp")]
100    service: Option<Arc<dyn McpService<Counterpart>>>,
101
102    /// The runner is a task that should be run alongside the message handler.
103    /// Some futures direct messages back through channels to this future which actually
104    /// handles responding to the client.
105    ///
106    /// Some connector implementations use this to run support tasks alongside
107    /// the message handler. It has no separate readiness protocol: communication
108    /// primitives needed by `connect` must already exist when the server is
109    /// constructed.
110    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    /// Create an MCP server from something that implements the [`McpServerConnect`](`super::McpServerConnect`) trait.
129    ///
130    /// # See also
131    ///
132    /// See `agent-client-protocol-rmcp` to construct MCP servers from Rust code
133    /// with `rmcp`.
134    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    /// Construct a reusable service for native MCP-over-ACP only.
145    #[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    /// Construct a reusable native service and a separate standalone factory.
155    ///
156    /// Native requests do not create standalone sessions or perform hidden
157    /// initialization/discovery exchanges.
158    #[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    /// Split this MCP server into the message handler and a future that must be run while the handler is active.
173    #[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    /// Split this MCP server into a protocol v2 session handler and its runner.
192    #[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/// Message handler created from a [`McpServer`].
238#[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    /// Append this MCP server's native ACP declaration to a session setup request.
277    fn append_declaration(&self, mcp_servers: &mut Vec<SchemaMcpServer>) {
278        mcp_servers.push(self.declaration());
279    }
280
281    /// Whether `request` is a valid v1 session setup request this server
282    /// attaches to. A setup request with malformed parameters is an error.
283    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/// Protocol v2 session adapter for the shared native MCP-over-ACP runtime.
308#[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    /// Append this server's declaration to a raw v2 session setup request.
384    ///
385    /// MCP server names must be unique within `mcpServers`. If the client
386    /// already declared a server with this server's name, this fails with
387    /// `-32602` and leaves the request unchanged.
388    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    /// Attach this server to a draft protocol v2 session setup request.
409    ///
410    /// MCP server names must be unique within a setup request's
411    /// `mcpServers`, and agents reject duplicates with `-32602`. A server
412    /// whose name is already declared fails here with `-32602` instead of
413    /// sending a request the agent must reject.
414    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/// The `name` of a v2 MCP server declaration, if it has one.
437#[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                    // Answer the client directly rather than forwarding a
473                    // request the agent must reject.
474                    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    /// Attach this server to the new session, spawning off a dynamic handler that will
498    /// manage requests coming from this session.
499    ///
500    /// # Return value
501    ///
502    /// Returns a [`DynamicHandlerGuard`] for the handler that intercepts messages
503    /// related to this MCP server. Once the value is dropped, the MCP server messages
504    /// will no longer be received, so you need to keep this value alive as long as the session
505    /// is in use. You can also invoke [`DynamicHandlerGuard::detach`] if you
506    /// want to keep the handler registered for the life of the connection.
507    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        // Edit the raw request so fields the v1 schema types do not model
534        // reach the agent unchanged.
535        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                            // when we receive a message from the server, fwd to the client
597                            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        // Fields the v1 schema types do not model must reach the agent.
906        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}