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    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
40/// A runtime-agnostic MCP server.
41///
42/// `McpServer` wraps an [`McpServerConnect`](`super::McpServerConnect`)
43/// implementation. A server whose counterpart is [`role::mcp::Client`] can be
44/// connected directly as a standalone MCP component. With the
45/// `unstable_mcp_over_acp` feature, servers can instead be attached to ACP
46/// session setup through `Builder::with_mcp_server` or
47/// `SessionBuilder::with_mcp_server`. When `unstable_protocol_v2` is also
48/// enabled, `Proxy.v2().with_mcp_server` attaches a server globally to draft
49/// v2 setup requests. `V2SessionBuilder::with_mcp_server` attaches one to a
50/// single new v2 session, while `V2ResumeSessionBuilder::with_mcp_server`
51/// attaches one to a single resumed session. With `unstable_session_fork`,
52/// `V2ForkSessionBuilder::with_mcp_server` attaches one to a single forked
53/// session.
54///
55/// # Creating an MCP Server
56///
57/// The `agent-client-protocol-rmcp` crate provides builder APIs for MCP tools
58/// backed by the `rmcp` crate.
59///
60/// Or implement [`McpServerConnect`](`super::McpServerConnect`) for custom server behavior:
61///
62/// ```rust,ignore
63/// let server = McpServer::new(MyCustomServerConnect, NullRun);
64/// ```
65pub struct McpServer<Counterpart: Role, Run = NullRun> {
66    /// The host role that is serving up this MCP server
67    phantom: PhantomData<Counterpart>,
68
69    /// The "connect" instance
70    connect: Arc<dyn McpServerConnect<Counterpart>>,
71
72    #[cfg(feature = "unstable_mcp_over_acp")]
73    service: Option<Arc<dyn McpService<Counterpart>>>,
74
75    /// The runner is a task that should be run alongside the message handler.
76    /// Some futures direct messages back through channels to this future which actually
77    /// handles responding to the client.
78    ///
79    /// Some connector implementations use this to run support tasks alongside
80    /// the message handler. It has no separate readiness protocol: communication
81    /// primitives needed by `connect` must already exist when the server is
82    /// constructed.
83    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    /// Create an MCP server from something that implements the [`McpServerConnect`](`super::McpServerConnect`) trait.
102    ///
103    /// # See also
104    ///
105    /// See `agent-client-protocol-rmcp` to construct MCP servers from Rust code
106    /// with `rmcp`.
107    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    /// Construct a reusable service for native MCP-over-ACP only.
118    #[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    /// Construct a reusable native service and a separate standalone factory.
128    ///
129    /// Native requests do not create standalone sessions or perform hidden
130    /// initialization/discovery exchanges.
131    #[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    /// Split this MCP server into the message handler and a future that must be run while the handler is active.
146    #[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    /// Split this MCP server into a protocol v2 session handler and its runner.
165    #[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/// Message handler created from a [`McpServer`].
211#[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    /// Append this MCP server's native ACP declaration to a session setup request.
243    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/// Protocol v2 session adapter for the shared native MCP-over-ACP runtime.
252#[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    /// Attach this server to a draft protocol v2 session setup request.
349    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    /// Attach this server to the new session, spawning off a dynamic handler that will
403    /// manage requests coming from this session.
404    ///
405    /// # Return value
406    ///
407    /// Returns a [`DynamicHandlerGuard`] for the handler that intercepts messages
408    /// related to this MCP server. Once the value is dropped, the MCP server messages
409    /// will no longer be received, so you need to keep this value alive as long as the session
410    /// is in use. You can also invoke [`DynamicHandlerGuard::detach`] if you
411    /// want to keep the handler registered for the life of the connection.
412    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                            // when we receive a message from the server, fwd to the client
534                            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}