context_harness/agent_runtime/
mcp_client.rs1use crate::{
4 agent_resource::Capability,
5 config::McpServerConfig,
6 tool_binding::{self, LoadedToolResource, ResolvedToolBinding},
7 traits::{Tool, ToolContext, ToolRegistry},
8};
9use anyhow::{anyhow, ensure, Result};
10use async_trait::async_trait;
11use futures_util::{SinkExt, StreamExt};
12use rmcp::{
13 model::{
14 CallToolRequestParams, ClientInfo, ClientResult, PaginatedRequestParams,
15 ServerNotification, ServerRequest, TaskSupport,
16 },
17 service::{NotificationContext, RequestContext, RunningService},
18 transport::Transport,
19 ErrorData, Peer, RoleClient, Service, ServiceExt,
20};
21use serde_json::Value;
22use sha2::{Digest, Sha256};
23use std::{collections::HashSet, path::Path, process::Stdio, sync::Arc, time::Duration};
24use tokio::{
25 process::{Child, ChildStdin, ChildStdout, Command},
26 sync::Mutex,
27};
28use tokio_util::codec::{FramedRead, FramedWrite, LinesCodec};
29
30const MAX_FRAME: usize = 1024 * 1024;
31const MAX_ARGUMENTS: usize = 64 * 1024;
32type Incoming = rmcp::service::RxJsonRpcMessage<RoleClient>;
33type Outgoing = rmcp::service::TxJsonRpcMessage<RoleClient>;
34struct BoundedTransport {
35 reader: FramedRead<ChildStdout, LinesCodec>,
36 writer: Arc<Mutex<Option<FramedWrite<ChildStdin, LinesCodec>>>>,
37 ended: bool,
38}
39impl Transport<RoleClient> for BoundedTransport {
40 type Error = std::io::Error;
41 fn send(
42 &mut self,
43 message: Outgoing,
44 ) -> impl std::future::Future<Output = std::io::Result<()>> + Send + 'static {
45 let writer = self.writer.clone();
46 async move {
47 let bytes = serde_json::to_vec(&message)
48 .map_err(|_| std::io::Error::other("MCP request encoding failed"))?;
49 if bytes.len() > MAX_FRAME {
50 return Err(std::io::Error::other("MCP request exceeds size limit"));
51 }
52 let mut writer = writer.lock().await;
53 writer
54 .as_mut()
55 .ok_or_else(|| std::io::Error::other("MCP transport closed"))?
56 .send(
57 String::from_utf8(bytes)
58 .map_err(|_| std::io::Error::other("MCP request encoding failed"))?,
59 )
60 .await
61 .map_err(|_| std::io::Error::other("MCP write failed"))
62 }
63 }
64 async fn receive(&mut self) -> Option<Incoming> {
65 if self.ended {
66 return None;
67 }
68 match self.reader.next().await {
69 Some(Ok(line)) => match serde_json::from_str(&line) {
70 Ok(message) => Some(message),
71 Err(_) => {
72 self.ended = true;
73 None
74 }
75 },
76 _ => {
77 self.ended = true;
78 None
79 }
80 }
81 }
82 async fn close(&mut self) -> std::io::Result<()> {
83 self.ended = true;
84 self.writer.lock().await.take();
85 Ok(())
86 }
87}
88struct EmptyClient;
90impl Service<RoleClient> for EmptyClient {
91 async fn handle_request(
92 &self,
93 _: ServerRequest,
94 _: RequestContext<RoleClient>,
95 ) -> Result<ClientResult, ErrorData> {
96 Err(ErrorData::invalid_request(
97 "client requests are unsupported",
98 None,
99 ))
100 }
101 async fn handle_notification(
102 &self,
103 _: ServerNotification,
104 _: NotificationContext<RoleClient>,
105 ) -> Result<(), ErrorData> {
106 Ok(())
107 }
108 fn get_info(&self) -> ClientInfo {
109 ClientInfo::default()
110 }
111}
112
113pub(super) struct Session {
114 child: Child,
116 service: RunningService<RoleClient, EmptyClient>,
117 tools: Vec<RemoteTool>,
118}
119impl Drop for Session {
120 fn drop(&mut self) {
121 self.service.cancellation_token().cancel();
122 let _ = self.child.start_kill();
123 }
124}
125impl Session {
126 pub(super) fn register(&self, registry: &mut ToolRegistry) -> Result<()> {
127 ensure!(
128 self.tools
129 .iter()
130 .all(|tool| registry.find(&tool.name).is_none()),
131 "duplicate MCP tool namespace"
132 );
133 for tool in &self.tools {
134 registry.register(Box::new(tool.clone()));
135 }
136 Ok(())
137 }
138 pub(super) async fn close(&mut self) {
139 let _ = self.child.start_kill();
140 let _ = self
141 .service
142 .close_with_timeout(Duration::from_secs(1))
143 .await;
144 let _ = tokio::time::timeout(Duration::from_secs(1), self.child.wait()).await;
145 }
146
147 pub(super) fn register_alias(
148 &self,
149 resource: &LoadedToolResource,
150 server: &McpServerConfig,
151 registry: &mut ToolRegistry,
152 ) -> Result<()> {
153 let (_, remote_name) =
154 tool_binding::mcp_reference(&resource.definition.tool.implementation)
155 .ok_or_else(|| anyhow!("invalid MCP alias implementation"))?;
156 let remote = self
157 .tools
158 .iter()
159 .find(|tool| tool.remote == remote_name)
160 .ok_or_else(|| anyhow!("MCP alias remote tool was not discovered"))?;
161 let binding = tool_binding::resolve_mcp_binding(
162 &resource.definition,
163 &remote.schema,
164 &remote.description,
165 &serde_json::to_value(server)?,
166 )?;
167 ensure!(
168 registry.find(&binding.name).is_none(),
169 "duplicate MCP alias"
170 );
171 registry.register(Box::new(RemoteAlias {
172 remote: remote.clone(),
173 binding,
174 }));
175 Ok(())
176 }
177}
178
179pub(super) async fn connect(name: &str, config: &McpServerConfig, root: &Path) -> Result<Session> {
180 config.validate(name)?;
181 let timeout = Duration::from_secs(config.timeout_seconds);
182 let mut child = Command::new(&config.command)
183 .args(&config.args)
184 .current_dir(root)
185 .stdin(Stdio::piped())
186 .stdout(Stdio::piped())
187 .stderr(Stdio::null())
188 .kill_on_drop(true)
189 .spawn()
190 .map_err(|_| anyhow!("MCP process could not start"))?;
191 let reader = child
192 .stdout
193 .take()
194 .ok_or_else(|| anyhow!("MCP stdout unavailable"))?;
195 let writer = child
196 .stdin
197 .take()
198 .ok_or_else(|| anyhow!("MCP stdin unavailable"))?;
199 let transport = BoundedTransport {
200 reader: FramedRead::new(reader, LinesCodec::new_with_max_length(MAX_FRAME)),
201 writer: Arc::new(Mutex::new(Some(FramedWrite::new(
202 writer,
203 LinesCodec::new_with_max_length(MAX_FRAME),
204 )))),
205 ended: false,
206 };
207 let service = tokio::time::timeout(timeout, EmptyClient.serve(transport))
208 .await
209 .map_err(|_| anyhow!("MCP initialization timed out"))?
210 .map_err(|_| anyhow!("MCP initialization failed"))?;
211 let mut session = Session {
212 child,
213 service,
214 tools: vec![],
215 };
216 let mut cursor = None;
217 let mut cursors = HashSet::new();
218 let mut names = HashSet::new();
219 for page in 0..16 {
220 let result = tokio::time::timeout(
221 timeout,
222 session
223 .service
224 .list_tools(Some(PaginatedRequestParams { meta: None, cursor })),
225 )
226 .await
227 .map_err(|_| anyhow!("MCP discovery timed out"))?
228 .map_err(|_| anyhow!("MCP discovery failed"))?;
229 ensure!(
230 session.tools.len() + result.tools.len() <= 128,
231 "MCP discovery exceeds tool limit"
232 );
233 for tool in result.tools {
234 ensure!(
235 !tool.name.is_empty()
236 && tool.name.len() <= 128
237 && tool
238 .name
239 .bytes()
240 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.')),
241 "invalid MCP tool name"
242 );
243 ensure!(
244 names.insert(tool.name.to_string()),
245 "duplicate MCP tool name"
246 );
247 ensure!(
248 tool.input_schema.get("type").and_then(Value::as_str) == Some("object"),
249 "MCP tool schema must declare object type"
250 );
251 ensure!(
252 tool.execution
253 .as_ref()
254 .and_then(|execution| execution.task_support)
255 != Some(TaskSupport::Required),
256 "MCP task-only tools are unsupported"
257 );
258 let public_name = format!("mcp.{name}.{}", tool.name);
259 let remote_name = tool.name.into_owned();
260 let schema = Value::Object((*tool.input_schema).clone());
261 let capabilities = vec![Capability::ProcessExecute, Capability::ExternalSideEffect];
262 let implementation_version = format!(
263 "sha256:{:x}",
264 Sha256::digest(serde_json::to_vec(&serde_json::json!({
265 "server": config,
266 "remote": remote_name,
267 "schema": schema,
268 }))?)
269 );
270 let metadata = tool_binding::compatibility_binding_metadata_versioned(
271 &public_name,
272 &public_name,
273 &implementation_version,
274 schema.clone(),
275 capabilities,
276 crate::tool_binding::ToolTrustClass::McpExternal,
277 );
278 session.tools.push(RemoteTool {
279 name: public_name,
280 remote: remote_name,
281 description: tool
282 .description
283 .map(|description| description.into_owned())
284 .unwrap_or_default(),
285 schema,
286 metadata,
287 peer: session.service.peer().clone(),
288 timeout,
289 });
290 }
291 cursor = result.next_cursor;
292 if let Some(next) = &cursor {
293 ensure!(
294 page < 15 && next.len() <= 4096 && cursors.insert(next.clone()),
295 "MCP discovery pagination limit"
296 );
297 } else {
298 return Ok(session);
299 }
300 }
301 unreachable!()
302}
303
304pub(super) fn validate_arguments(arguments: &Value) -> Result<()> {
305 ensure!(arguments.is_object(), "MCP arguments must be an object");
306 ensure!(
307 serde_json::to_vec(arguments)?.len() <= MAX_ARGUMENTS,
308 "MCP arguments exceed size limit"
309 );
310 Ok(())
311}
312#[derive(Clone)]
313struct RemoteTool {
314 name: String,
315 remote: String,
316 description: String,
317 schema: Value,
318 metadata: Value,
319 peer: Peer<RoleClient>,
320 timeout: Duration,
321}
322
323#[derive(Clone)]
324struct RemoteAlias {
325 remote: RemoteTool,
326 binding: ResolvedToolBinding,
327}
328
329impl RemoteAlias {
330 fn effective_arguments(&self, arguments: Value) -> Result<Value> {
331 tool_binding::validate_binding_arguments(&self.binding.public_schema, &arguments)?;
332 let mut arguments = arguments
333 .as_object()
334 .ok_or_else(|| anyhow!("MCP arguments must be an object"))?
335 .clone();
336 for (name, value) in self
337 .binding
338 .fixed
339 .as_object()
340 .ok_or_else(|| anyhow!("fixed arguments must be an object"))?
341 {
342 ensure!(
343 !arguments.contains_key(name),
344 "fixed argument cannot be overridden"
345 );
346 arguments.insert(name.clone(), value.clone());
347 }
348 let arguments = Value::Object(arguments);
349 tool_binding::validate_binding_arguments(&self.remote.schema, &arguments)?;
350 Ok(arguments)
351 }
352}
353
354#[async_trait]
355impl Tool for RemoteAlias {
356 fn name(&self) -> &str {
357 &self.binding.name
358 }
359 fn description(&self) -> &str {
360 &self.binding.description
361 }
362 fn parameters_schema(&self) -> Value {
363 self.binding.public_schema.clone()
364 }
365 fn capabilities(&self) -> Option<Vec<Capability>> {
366 Some(self.binding.capabilities.clone())
367 }
368 fn binding_metadata(&self) -> Option<Value> {
369 let mut metadata = tool_binding::binding_metadata(&self.binding);
370 metadata["remote_metadata"] = serde_json::json!(self.binding.remote_metadata);
371 Some(metadata)
372 }
373 fn validate_arguments(&self, arguments: &Value) -> Result<()> {
374 tool_binding::validate_binding_arguments(&self.binding.public_schema, arguments)
375 }
376 fn approval_arguments(&self, arguments: &Value) -> Result<Value> {
377 self.effective_arguments(arguments.clone())
378 }
379 async fn execute(&self, arguments: Value, context: &ToolContext) -> Result<Value> {
380 let result = self
381 .remote
382 .execute(self.effective_arguments(arguments)?, context)
383 .await?;
384 let limit = self
385 .binding
386 .restrictions
387 .max_output_bytes
388 .unwrap_or(MAX_FRAME as u64);
389 ensure!(
390 serde_json::to_vec(&result)?.len() as u64 <= limit,
391 "MCP alias result exceeds output limit"
392 );
393 Ok(result)
394 }
395}
396#[async_trait]
397impl Tool for RemoteTool {
398 fn name(&self) -> &str {
399 &self.name
400 }
401 fn description(&self) -> &str {
402 &self.description
403 }
404 fn parameters_schema(&self) -> Value {
405 self.schema.clone()
406 }
407 fn capabilities(&self) -> Option<Vec<Capability>> {
408 Some(vec![
409 Capability::ProcessExecute,
410 Capability::ExternalSideEffect,
411 ])
412 }
413 fn binding_metadata(&self) -> Option<Value> {
414 Some(self.metadata.clone())
415 }
416 fn validate_arguments(&self, arguments: &Value) -> Result<()> {
417 validate_arguments(arguments)
418 }
419 async fn execute(&self, arguments: Value, _: &ToolContext) -> Result<Value> {
420 validate_arguments(&arguments)?;
421 let result = tokio::time::timeout(
422 self.timeout,
423 self.peer.call_tool(CallToolRequestParams {
424 meta: None,
425 name: self.remote.clone().into(),
426 arguments: arguments.as_object().cloned(),
427 task: None,
428 }),
429 )
430 .await
431 .map_err(|_| anyhow!("MCP tool call timed out"))?
432 .map_err(|_| anyhow!("MCP tool call failed"))?;
433 ensure!(result.is_error != Some(true), "MCP tool returned an error");
434 let bytes =
435 serde_json::to_vec(&result).map_err(|_| anyhow!("MCP result encoding failed"))?;
436 ensure!(bytes.len() <= MAX_FRAME, "MCP result exceeds size limit");
437 serde_json::from_slice(&bytes).map_err(|_| anyhow!("MCP result encoding failed"))
438 }
439}