Skip to main content

agent_client_protocol_rmcp/
builder.rs

1//! MCP server builder for creating MCP servers.
2
3use std::{future::Future, marker::PhantomData, pin::pin, sync::Arc};
4
5use futures::future::{BoxFuture, Either};
6use futures_concurrency::future::TryJoin;
7use rmcp::{
8    ErrorData, ServerHandler,
9    model::{
10        CacheScope, CallToolResponse, CallToolResult, ListToolsResult, ProtocolVersion,
11        ServerConfig, Tool,
12    },
13};
14use schemars::JsonSchema;
15use serde::{Serialize, de::DeserializeOwned};
16use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
17
18#[cfg(feature = "unstable_mcp_over_acp")]
19use acp::mcp_server::{McpOutcome, McpRequest, McpRequestContext, McpService};
20use agent_client_protocol as acp;
21use agent_client_protocol::{
22    ByteStreams, ChainRun, ConnectTo, DynConnectTo, NullRun, RunWithConnectionTo,
23    mcp_server::{
24        McpConnectionTo, McpServer, McpServerConnect, McpTool, McpToolMetadata, McpToolRegistry,
25    },
26    role::{self, Role},
27};
28
29/// Builder for creating MCP servers with tools.
30///
31/// Use [`crate::McpServerExt::builder`] to create a new builder, then chain methods to
32/// configure the server and call [`build`](Self::build) to create the server.
33///
34/// # Example
35///
36/// ```rust,ignore
37/// use agent_client_protocol::mcp_server::McpServer;
38/// use agent_client_protocol_rmcp::McpServerExt;
39///
40/// let server = McpServer::builder("my-server".to_string())
41///     .instructions("A helpful assistant")
42///     .tool(EchoTool)
43///     .tool_fn(
44///         "greet",
45///         "Greet someone by name",
46///         async |input: GreetInput, _cx| Ok(format!("Hello, {}!", input.name)),
47///         agent_client_protocol_rmcp::tool_fn!(),
48///     )
49///     .build();
50/// ```
51#[derive(Debug)]
52pub struct McpServerBuilder<Counterpart: Role, Runner>
53where
54    Runner: RunWithConnectionTo<Counterpart>,
55{
56    phantom: PhantomData<Counterpart>,
57    name: String,
58    data: McpToolRegistry<Counterpart>,
59    runner: Runner,
60}
61
62impl<Counterpart: Role> McpServerBuilder<Counterpart, NullRun> {
63    pub(super) fn new(name: String) -> Self {
64        Self {
65            name,
66            phantom: PhantomData,
67            data: McpToolRegistry::default(),
68            runner: NullRun,
69        }
70    }
71}
72
73impl<Counterpart: Role, Runner> McpServerBuilder<Counterpart, Runner>
74where
75    Runner: RunWithConnectionTo<Counterpart>,
76{
77    /// Set the server instructions that are provided to the client.
78    #[must_use]
79    pub fn instructions(mut self, instructions: impl ToString) -> Self {
80        self.data.set_instructions(instructions);
81        self
82    }
83
84    /// Add a tool to the server.
85    #[must_use]
86    pub fn tool(mut self, tool: impl McpTool<Counterpart> + 'static) -> Self {
87        self.data.register_tool(tool);
88        self
89    }
90
91    /// Disable all tools. After calling this, only tools explicitly enabled
92    /// with [`enable_tool`](Self::enable_tool) will be available.
93    #[must_use]
94    pub fn disable_all_tools(mut self) -> Self {
95        self.data.disable_all_tools();
96        self
97    }
98
99    /// Enable all tools. After calling this, all tools will be available
100    /// except those explicitly disabled with [`disable_tool`](Self::disable_tool).
101    #[must_use]
102    pub fn enable_all_tools(mut self) -> Self {
103        self.data.enable_all_tools();
104        self
105    }
106
107    /// Disable a specific tool by name.
108    ///
109    /// Returns an error if the tool is not registered.
110    pub fn disable_tool(mut self, name: &str) -> Result<Self, acp::Error> {
111        self.data.disable_tool(name)?;
112        Ok(self)
113    }
114
115    /// Enable a specific tool by name.
116    ///
117    /// Returns an error if the tool is not registered.
118    pub fn enable_tool(mut self, name: &str) -> Result<Self, acp::Error> {
119        self.data.enable_tool(name)?;
120        Ok(self)
121    }
122
123    /// Private fn: adds the tool but also adds a runner that will be
124    /// run while the MCP server is active.
125    fn tool_with_runner(
126        self,
127        tool: impl McpTool<Counterpart> + 'static,
128        tool_runner: impl RunWithConnectionTo<Counterpart>,
129    ) -> McpServerBuilder<Counterpart, impl RunWithConnectionTo<Counterpart>> {
130        let this = self.tool(tool);
131        McpServerBuilder {
132            phantom: PhantomData,
133            name: this.name,
134            data: this.data,
135            runner: ChainRun::new(this.runner, tool_runner),
136        }
137    }
138
139    /// Convenience wrapper for defining a "single-threaded" tool without having to create a struct.
140    /// By "single-threaded", we mean that only one invocation of the tool can be running at a time.
141    /// Typically agents invoke a tool once per session and then block waiting for the result,
142    /// so this is fine, but they could attempt to run multiple invocations concurrently, in which
143    /// case those invocations would be serialized.
144    ///
145    /// # Parameters
146    ///
147    /// * `name`: The name of the tool.
148    /// * `description`: The description of the tool.
149    /// * `func`: The function that implements the tool. Use an async closure like `async |args, cx| { .. }`.
150    ///
151    /// # Examples
152    ///
153    /// ```rust,ignore
154    /// McpServer::builder("my-server")
155    ///     .tool_fn_mut(
156    ///         "greet",
157    ///         "Greet someone by name",
158    ///         async |input: GreetInput, _cx| Ok(format!("Hello, {}!", input.name)),
159    ///     )
160    /// ```
161    pub fn tool_fn_mut<P, Ret, F>(
162        self,
163        name: impl ToString,
164        description: impl ToString,
165        func: F,
166        tool_future_hack: impl for<'a> Fn(
167            &'a mut F,
168            P,
169            McpConnectionTo<Counterpart>,
170        ) -> BoxFuture<'a, Result<Ret, acp::Error>>
171        + Send
172        + 'static,
173    ) -> McpServerBuilder<Counterpart, impl RunWithConnectionTo<Counterpart>>
174    where
175        P: JsonSchema + DeserializeOwned + 'static + Send,
176        Ret: JsonSchema + Serialize + 'static + Send,
177        F: AsyncFnMut(P, McpConnectionTo<Counterpart>) -> Result<Ret, acp::Error> + Send,
178    {
179        let (tool, runner) =
180            acp::mcp_server::tool_fn_mut(name, description, func, tool_future_hack);
181        self.tool_with_runner(tool, runner)
182    }
183
184    /// Convenience wrapper for defining a stateless tool that can run concurrently.
185    /// Unlike [`tool_fn_mut`](Self::tool_fn_mut), multiple invocations of this tool can run
186    /// at the same time since the function is `Fn` rather than `FnMut`.
187    ///
188    /// # Parameters
189    ///
190    /// * `name`: The name of the tool.
191    /// * `description`: The description of the tool.
192    /// * `func`: The function that implements the tool. Use an async closure like `async |args, cx| { .. }`.
193    ///
194    /// # Examples
195    ///
196    /// ```rust,ignore
197    /// McpServer::builder("my-server")
198    ///     .tool_fn(
199    ///         "greet",
200    ///         "Greet someone by name",
201    ///         async |input: GreetInput, _cx| Ok(format!("Hello, {}!", input.name)),
202    ///     )
203    /// ```
204    pub fn tool_fn<P, Ret, F>(
205        self,
206        name: impl ToString,
207        description: impl ToString,
208        func: F,
209        tool_future_hack: impl for<'a> Fn(
210            &'a F,
211            P,
212            McpConnectionTo<Counterpart>,
213        ) -> BoxFuture<'a, Result<Ret, acp::Error>>
214        + Send
215        + Sync
216        + 'static,
217    ) -> McpServerBuilder<Counterpart, impl RunWithConnectionTo<Counterpart>>
218    where
219        P: JsonSchema + DeserializeOwned + 'static + Send,
220        Ret: JsonSchema + Serialize + 'static + Send,
221        F: AsyncFn(P, McpConnectionTo<Counterpart>) -> Result<Ret, acp::Error>
222            + Send
223            + Sync
224            + 'static,
225    {
226        let (tool, runner) = acp::mcp_server::tool_fn(name, description, func, tool_future_hack);
227        self.tool_with_runner(tool, runner)
228    }
229
230    /// Create an MCP server from this builder.
231    ///
232    /// This builder can be served directly. With the `unstable_mcp_over_acp`
233    /// feature, it can also be attached through
234    /// `SessionBuilder::with_mcp_server` or `Builder::with_mcp_server`.
235    pub fn build(self) -> McpServer<Counterpart, Runner> {
236        let built = McpServerBuilt {
237            name: self.name,
238            data: Arc::new(self.data),
239        };
240        #[cfg(feature = "unstable_mcp_over_acp")]
241        {
242            let standalone = McpServerBuilt {
243                name: built.name.clone(),
244                data: built.data.clone(),
245            };
246            McpServer::new_service_with_standalone(built, standalone, self.runner)
247        }
248        #[cfg(not(feature = "unstable_mcp_over_acp"))]
249        {
250            McpServer::new(built, self.runner)
251        }
252    }
253}
254
255struct McpServerBuilt<Counterpart: Role> {
256    name: String,
257    data: Arc<McpToolRegistry<Counterpart>>,
258}
259
260#[cfg(feature = "unstable_mcp_over_acp")]
261impl<Counterpart: Role> McpService<Counterpart> for McpServerBuilt<Counterpart> {
262    fn execute(
263        &self,
264        request: McpRequest,
265        context: McpRequestContext<Counterpart>,
266    ) -> BoxFuture<'static, Result<McpOutcome, acp::Error>> {
267        crate::native::execute(
268            Arc::new(McpServerConnection {
269                data: self.data.clone(),
270                mcp_connection: context.connection().clone(),
271            }),
272            request,
273            context,
274        )
275    }
276}
277
278impl<Counterpart: Role> McpServerConnect<Counterpart> for McpServerBuilt<Counterpart> {
279    fn name(&self) -> String {
280        self.name.clone()
281    }
282
283    fn connect(
284        &self,
285        mcp_connection: McpConnectionTo<Counterpart>,
286    ) -> DynConnectTo<role::mcp::Client> {
287        DynConnectTo::new(McpServerConnection {
288            data: self.data.clone(),
289            mcp_connection,
290        })
291    }
292}
293
294/// A connected MCP server instance.
295pub(crate) struct McpServerConnection<Counterpart: Role> {
296    data: Arc<McpToolRegistry<Counterpart>>,
297    mcp_connection: McpConnectionTo<Counterpart>,
298}
299
300impl<Counterpart: Role> ConnectTo<role::mcp::Client> for McpServerConnection<Counterpart> {
301    async fn connect_to(self, client: impl ConnectTo<role::mcp::Server>) -> Result<(), acp::Error> {
302        // Create tokio byte streams that rmcp expects
303        let (mcp_server_stream, mcp_client_stream) = tokio::io::duplex(8192);
304        let (mcp_server_read, mcp_server_write) = tokio::io::split(mcp_server_stream);
305        let (mcp_client_read, mcp_client_write) = tokio::io::split(mcp_client_stream);
306
307        let run_client = async {
308            let byte_streams =
309                ByteStreams::new(mcp_client_write.compat_write(), mcp_client_read.compat());
310            <ByteStreams<_, _> as ConnectTo<role::mcp::Client>>::connect_to(byte_streams, client)
311                .await
312        };
313
314        let run_server = async {
315            // Run the rmcp server with the server side of the duplex stream
316            let running_server = rmcp::ServiceExt::serve(self, (mcp_server_read, mcp_server_write))
317                .await
318                .map_err(acp::Error::into_internal_error)?;
319
320            // Wait for the server to finish
321            running_server
322                .waiting()
323                .await
324                .map(|_quit_reason| ())
325                .map_err(acp::Error::into_internal_error)
326        };
327
328        (run_client, run_server).try_join().await?;
329        Ok(())
330    }
331}
332
333impl<R: Role> ServerHandler for McpServerConnection<R> {
334    async fn call_tool(
335        &self,
336        request: rmcp::model::CallToolRequestParams,
337        context: rmcp::service::RequestContext<rmcp::RoleServer>,
338    ) -> Result<CallToolResponse, ErrorData> {
339        // Lookup the tool definition, erroring if not found or disabled
340        let Some(registered) = self.data.enabled_tool(&request.name) else {
341            return Err(rmcp::model::ErrorData::invalid_params(
342                format!("tool `{}` not found", request.name),
343                None,
344            ));
345        };
346
347        // Convert input into JSON
348        let serde_value = serde_json::to_value(request.arguments).expect("valid json");
349
350        // Execute the user's tool, unless cancellation occurs
351        let has_structured_output = registered.has_structured_output();
352        match futures::future::select(
353            registered.call_tool(serde_value, self.mcp_connection.clone()),
354            pin!(context.ct.cancelled()),
355        )
356        .await
357        {
358            // If completed successfully
359            Either::Left((m, _)) => match m {
360                Ok(result) => {
361                    // Use structured output only if the tool declared an output_schema
362                    if has_structured_output {
363                        Ok(CallToolResult::structured(result).into())
364                    } else {
365                        let content = rmcp::model::ContentBlock::text(result.to_string());
366                        Ok(CallToolResult::success(vec![content]).into())
367                    }
368                }
369                Err(error) => Err(to_rmcp_error(error)),
370            },
371
372            // If cancelled
373            Either::Right(((), _)) => {
374                Err(rmcp::ErrorData::internal_error("operation cancelled", None))
375            }
376        }
377    }
378
379    fn list_tools(
380        &self,
381        _request: Option<rmcp::model::PaginatedRequestParams>,
382        context: rmcp::service::RequestContext<rmcp::RoleServer>,
383    ) -> impl Future<Output = Result<rmcp::model::ListToolsResult, ErrorData>> + Send {
384        // Return only enabled tools
385        let tools: Vec<_> = self
386            .data
387            .enabled_tools()
388            .map(|tool| make_tool_model(tool.metadata()))
389            .collect();
390        let mut result = ListToolsResult::with_all_items(tools);
391        if context
392            .protocol_version()
393            .is_some_and(|version| version >= ProtocolVersion::V_2026_07_28)
394        {
395            // Modern MCP requires cache metadata. Avoid sharing tool catalogs
396            // across authorization contexts or promising a stale cache lifetime.
397            result = result.with_ttl_ms(0).with_cache_scope(CacheScope::Private);
398        }
399        std::future::ready(Ok(result))
400    }
401
402    fn get_info(&self) -> ServerConfig {
403        // Basic server info
404        let base = ServerConfig::new(
405            rmcp::model::ServerCapabilities::builder()
406                .enable_tools()
407                .build(),
408        )
409        .with_server_info(rmcp::model::Implementation::default())
410        .with_protocol_version(rmcp::model::ProtocolVersion::default());
411
412        if let Some(instructions) = self.data.instructions() {
413            base.with_instructions(instructions.to_string())
414        } else {
415            base
416        }
417    }
418}
419
420/// Create an `rmcp` tool model from runtime-neutral MCP tool metadata.
421fn make_tool_model(metadata: &McpToolMetadata) -> Tool {
422    let mut tool = rmcp::model::Tool::new(
423        metadata.name().to_string(),
424        metadata.description().to_string(),
425        metadata.input_schema().clone(),
426    );
427
428    if let Some(title) = metadata.title() {
429        tool = tool.with_title(title.to_string());
430    }
431
432    if let Some(schema) = metadata.output_schema() {
433        tool = tool.with_raw_output_schema(schema.clone());
434    }
435
436    tool
437}
438
439/// Convert an [`agent_client_protocol::Error`] into an [`rmcp::ErrorData`].
440fn to_rmcp_error(error: acp::Error) -> rmcp::ErrorData {
441    rmcp::ErrorData {
442        code: rmcp::model::ErrorCode(error.code.into()),
443        message: error.message.into(),
444        data: error.data,
445    }
446}