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 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
38/// A runtime-agnostic MCP server.
39///
40/// `McpServer` wraps an [`McpServerConnect`](`super::McpServerConnect`)
41/// implementation. A server whose counterpart is [`role::mcp::Client`] can be
42/// connected directly as a standalone MCP component. With the
43/// `unstable_mcp_over_acp` feature, servers can instead be attached to ACP
44/// session setup through `Builder::with_mcp_server` or
45/// `SessionBuilder::with_mcp_server`. When `unstable_protocol_v2` is also
46/// enabled, `Proxy.v2().with_mcp_server` attaches a server globally to draft
47/// v2 setup requests. `V2SessionBuilder::with_mcp_server` attaches one to a
48/// single new v2 session, while `V2ResumeSessionBuilder::with_mcp_server`
49/// attaches one to a single resumed session. With `unstable_session_fork`,
50/// `V2ForkSessionBuilder::with_mcp_server` attaches one to a single forked
51/// session.
52///
53/// # Creating an MCP Server
54///
55/// The `agent-client-protocol-rmcp` crate provides builder APIs for MCP tools
56/// backed by the `rmcp` crate.
57///
58/// Or implement [`McpServerConnect`](`super::McpServerConnect`) for custom server behavior:
59///
60/// ```rust,ignore
61/// let server = McpServer::new(MyCustomServerConnect, NullRun);
62/// ```
63pub struct McpServer<Counterpart: Role, Run = NullRun> {
64    /// The host role that is serving up this MCP server
65    phantom: PhantomData<Counterpart>,
66
67    /// The "connect" instance
68    connect: Arc<dyn McpServerConnect<Counterpart>>,
69
70    /// The runner is a task that should be run alongside the message handler.
71    /// Some futures direct messages back through channels to this future which actually
72    /// handles responding to the client.
73    ///
74    /// Some connector implementations use this to run support tasks alongside
75    /// the message handler. It has no separate readiness protocol: communication
76    /// primitives needed by `connect` must already exist when the server is
77    /// constructed.
78    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    /// Create an MCP server from something that implements the [`McpServerConnect`](`super::McpServerConnect`) trait.
97    ///
98    /// # See also
99    ///
100    /// See `agent-client-protocol-rmcp` to construct MCP servers from Rust code
101    /// with `rmcp`.
102    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    /// Split this MCP server into the message handler and a future that must be run while the handler is active.
111    #[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    /// Split this MCP server into a protocol v2 session handler and its runner.
126    #[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/// Message handler created from a [`McpServer`].
142#[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    /// Append this MCP server's native ACP declaration to a session setup request.
166    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/// Protocol v2 session adapter for the shared native MCP-over-ACP runtime.
175#[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    /// Attach this server to a draft protocol v2 session setup request.
259    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    /// Attach this server to the new session, spawning off a dynamic handler that will
310    /// manage requests coming from this session.
311    ///
312    /// # Return value
313    ///
314    /// Returns a [`DynamicHandlerGuard`] for the handler that intercepts messages
315    /// related to this MCP server. Once the value is dropped, the MCP server messages
316    /// will no longer be received, so you need to keep this value alive as long as the session
317    /// is in use. You can also invoke [`DynamicHandlerGuard::detach`] if you
318    /// want to keep the handler registered for the life of the connection.
319    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                            // when we receive a message from the server, fwd to the client
434                            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}