context_harness/agent_runtime/
mcp.rs1use super::*;
4use crate::agent_resource::Capability;
5use std::collections::BTreeSet;
6
7pub(super) const CAPABILITIES: [Capability; 2] =
8 [Capability::ProcessExecute, Capability::ExternalSideEffect];
9
10impl AgentRuntime {
11 pub(super) async fn external_tools(
12 &self,
13 id: &str,
14 resource: &LoadedAgentResource,
15 ) -> Result<(ToolRegistry, Vec<mcp_client::Session>)> {
16 let mut servers = BTreeSet::new();
17 for name in &resource.definition.agent.tools {
20 let implementation = self
21 .tool_bindings
22 .get(name)
23 .map(|binding| binding.definition.tool.implementation.as_str())
24 .unwrap_or(name);
25 if let Some((server, tool)) = tool_binding::mcp_reference(implementation) {
26 ensure!(!tool.is_empty(), "MCP tool name is empty");
27 let config = self
28 .config
29 .mcp_servers
30 .get(server)
31 .context("MCP server is not configured")?;
32 config.validate(server)?;
33 ensure!(
34 self.policy
35 .authorize(&resource.definition.agent.permissions, &CAPABILITIES)
36 != Authorization::Denied,
37 "MCP tools require process_execute and external_side_effect permissions"
38 );
39 servers.insert(server.to_owned());
40 } else {
41 let tool = self
42 .tools
43 .find(name)
44 .context("unsupported runtime tool declaration")?;
45 if matches!(tool.runtime_dispatch(), ToolRuntimeDispatch::RunControl(_)) {
46 continue;
47 }
48 let capabilities = tool
49 .capabilities()
50 .context("tool capability metadata is unavailable")?;
51 ensure!(
52 self.policy
53 .authorize(&resource.definition.agent.permissions, &capabilities)
54 != Authorization::Denied,
55 "tool capability is not permitted by agent and host policy"
56 );
57 }
58 }
59 ensure!(
60 servers.len() <= 8,
61 "at most 8 MCP servers may be used in one run"
62 );
63 let mut registry = ToolRegistry::new();
64 let mut sessions = Vec::new();
65 for name in servers {
66 let config = &self.config.mcp_servers[&name];
67 let call_id = format!("mcp-start-{}", uuid::Uuid::new_v4());
68 let tool = format!("runtime.mcp.start.{name}");
69 let arguments =
70 json!({"server":name,"command":config.command,"args":config.args,"cwd":self.root});
71 self.store
72 .request_tool(id, &call_id, &tool, &arguments)
73 .await?;
74 let authorization = self
75 .policy
76 .authorize(&resource.definition.agent.permissions, &CAPABILITIES);
77 self.approve_invocation(id, &call_id, &tool, &arguments, authorization)
78 .await?;
79 self.store.start_tool(id, &call_id).await?;
80 let result = mcp_client::connect(&name, config, &self.root).await;
81 match result {
82 Ok(session) => {
83 if session.register(&mut registry).is_err() {
84 self.store
85 .finish_tool(
86 id,
87 &call_id,
88 ToolOutcome::Failed("MCP discovery failed".into()),
89 )
90 .await?;
91 anyhow::bail!("MCP discovery failed");
92 }
93 for binding in resource
94 .definition
95 .agent
96 .tools
97 .iter()
98 .filter_map(|tool| self.tool_bindings.get(tool))
99 .filter(|binding| {
100 tool_binding::mcp_reference(&binding.definition.tool.implementation)
101 .is_some_and(|(binding_server, _)| binding_server == name)
102 })
103 {
104 if session
105 .register_alias(binding, config, &mut registry)
106 .is_err()
107 {
108 self.store
109 .finish_tool(
110 id,
111 &call_id,
112 ToolOutcome::Failed(
113 "MCP alias schema validation failed".into(),
114 ),
115 )
116 .await?;
117 anyhow::bail!("MCP alias schema validation failed");
118 }
119 }
120 self.store
121 .finish_tool(
122 id,
123 &call_id,
124 ToolOutcome::Completed(json!({"server":name,"connected":true})),
125 )
126 .await?;
127 sessions.push(session);
128 }
129 Err(_) => {
130 self.store
131 .finish_tool(
132 id,
133 &call_id,
134 ToolOutcome::Failed("MCP connection or discovery failed".into()),
135 )
136 .await?;
137 anyhow::bail!("MCP connection or discovery failed");
138 }
139 }
140 }
141 Ok((registry, sessions))
142 }
143
144 pub(super) async fn approve_invocation(
145 &self,
146 id: &str,
147 call_id: &str,
148 tool: &str,
149 arguments: &serde_json::Value,
150 authorization: Authorization,
151 ) -> Result<()> {
152 if authorization == Authorization::Denied {
153 self.store
154 .finish_tool(
155 id,
156 call_id,
157 ToolOutcome::Denied("tool capability denied".into()),
158 )
159 .await?;
160 anyhow::bail!("tool capability denied");
161 }
162 if let Authorization::Approval(capabilities) = authorization {
163 self.store
164 .request_approval(id, call_id, &capabilities)
165 .await?;
166 let granted = self
167 .approvals
168 .approve(&ApprovalRequest {
169 run_id: id.into(),
170 call_id: call_id.into(),
171 tool: tool.into(),
172 capabilities,
173 arguments: arguments.clone(),
174 })
175 .await;
176 self.store.decide_approval(id, call_id, granted).await?;
177 if !granted {
178 self.store
179 .finish_tool(id, call_id, ToolOutcome::Denied("approval denied".into()))
180 .await?;
181 anyhow::bail!("tool approval denied");
182 }
183 }
184 Ok(())
185 }
186}