Skip to main content

context_harness/agent_runtime/
mcp.rs

1//! Per-run external tool discovery. Even initialization starts an unsandboxed
2//! process, so it passes through durable authorization before connecting.
3use 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        // Validate every declaration before starting any process, including known
18        // builtins, so a denied or missing declaration cannot trigger startup.
19        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}