1use std::time::Duration;
2
3#[cfg(feature = "stream")]
4use sim_codec_mcp::McpEnvelope;
5use sim_kernel::{
6 Args, CapabilityName, Consistency, Cx, Error, EvalMode, EvalRequest, Expr, Result, ShapeId,
7 ShapeRef, Symbol, Value,
8};
9use sim_shape::check_value_report;
10
11use crate::content::{McpCallParams, McpToolResult, arguments_to_values};
12use crate::{McpSession, McpSurfaceCard, McpSurfaceRole, project_mcp_surface};
13
14pub fn mcp_tools_call_capability() -> CapabilityName {
16 CapabilityName::new("mcp.tools.call")
17}
18
19pub fn mcp_resources_read_capability() -> CapabilityName {
21 CapabilityName::new("mcp.resources.read")
22}
23
24pub fn mcp_prompts_get_capability() -> CapabilityName {
26 CapabilityName::new("mcp.prompts.get")
27}
28
29pub(crate) fn execute_tool_call(
30 cx: &mut Cx,
31 session: &McpSession,
32 params: McpCallParams,
33) -> Result<McpToolResult> {
34 let row = resolve_tool_row(cx, session, ¶ms.name)?;
35 execute_surface_call(
36 cx,
37 session,
38 &row,
39 mcp_tools_call_capability(),
40 params.arguments,
41 "MCP tool",
42 )
43}
44
45#[cfg(feature = "stream")]
46pub(crate) fn execute_tool_call_with_stream(
47 cx: &mut Cx,
48 session: &mut McpSession,
49 params: McpCallParams,
50 progress_token: Option<&Expr>,
51) -> Result<(McpToolResult, Vec<McpEnvelope>)> {
52 let row = resolve_tool_row(cx, session, ¶ms.name)?;
53 execute_surface_call_with_stream(
54 cx,
55 session,
56 &row,
57 mcp_tools_call_capability(),
58 params.arguments,
59 "MCP tool",
60 progress_token,
61 )
62}
63
64pub(crate) fn execute_surface_call(
65 cx: &mut Cx,
66 session: &McpSession,
67 row: &McpSurfaceCard,
68 gate_capability: CapabilityName,
69 arguments: Vec<Expr>,
70 label: &'static str,
71) -> Result<McpToolResult> {
72 let request = eval_request_for_row(
73 row,
74 gate_capability,
75 arguments.clone(),
76 session.deadline_ms,
77 label,
78 )?;
79 require_session_capabilities(session, &request.required_capabilities)?;
80 let values = arguments_to_values(cx, &arguments)?;
81 validate_input(cx, row.input_shape.as_ref(), &values)?;
82 match call_local(cx, row, &request, values) {
83 Ok(value) => McpToolResult::success(cx, value),
84 Err(error) => Ok(McpToolResult::error(error.to_string())),
85 }
86}
87
88#[cfg(feature = "stream")]
89pub(crate) fn execute_surface_call_with_stream(
90 cx: &mut Cx,
91 session: &mut McpSession,
92 row: &McpSurfaceCard,
93 gate_capability: CapabilityName,
94 arguments: Vec<Expr>,
95 label: &'static str,
96 progress_token: Option<&Expr>,
97) -> Result<(McpToolResult, Vec<McpEnvelope>)> {
98 let request = eval_request_for_row(
99 row,
100 gate_capability,
101 arguments.clone(),
102 session.deadline_ms,
103 label,
104 )?;
105 require_session_capabilities(session, &request.required_capabilities)?;
106 let values = arguments_to_values(cx, &arguments)?;
107 validate_input(cx, row.input_shape.as_ref(), &values)?;
108 match call_local(cx, row, &request, values) {
109 Ok(value) => {
110 let drain = crate::stream::drain_value_stream(cx, &value, progress_token)?;
111 session.record_stream_packets(drain.packets);
112 Ok((McpToolResult::success(cx, value)?, drain.notifications))
113 }
114 Err(error) => Ok((McpToolResult::error(error.to_string()), Vec::new())),
115 }
116}
117
118pub(crate) fn require_surface_capabilities(
119 session: &McpSession,
120 row: &McpSurfaceCard,
121 gate_capability: CapabilityName,
122) -> Result<()> {
123 let required = required_capabilities_for_row(row, gate_capability);
124 require_session_capabilities(session, &required)
125}
126
127fn resolve_tool_row(cx: &mut Cx, session: &McpSession, name: &str) -> Result<McpSurfaceCard> {
128 project_mcp_surface(cx, &session.native_cards, &session.profile)?
129 .into_iter()
130 .find(|row| row.role == McpSurfaceRole::Tool && row.name == name)
131 .ok_or_else(|| Error::Eval(format!("unknown MCP tool {name}")))
132}
133
134fn eval_request_for_row(
135 row: &McpSurfaceCard,
136 gate_capability: CapabilityName,
137 arguments: Vec<Expr>,
138 deadline_ms: Option<u64>,
139 label: &'static str,
140) -> Result<EvalRequest> {
141 let symbol = row
142 .symbol
143 .clone()
144 .ok_or_else(|| Error::Eval(format!("{label} {} has no callable symbol", row.name)))?;
145 let required_capabilities = required_capabilities_for_row(row, gate_capability);
146 Ok(EvalRequest {
147 expr: Expr::Call {
148 operator: Box::new(Expr::Symbol(symbol)),
149 args: arguments,
150 },
151 result_shape: row.output_shape.clone(),
152 required_capabilities,
153 deadline: deadline_ms.map(Duration::from_millis),
154 consistency: Consistency::LocalFirst,
155 mode: EvalMode::Eval,
156 answer_limit: None,
157 stream_buffer: None,
158 stream: false,
159 trace: false,
160 })
161}
162
163fn required_capabilities_for_row(
164 row: &McpSurfaceCard,
165 gate_capability: CapabilityName,
166) -> Vec<CapabilityName> {
167 let mut required_capabilities = vec![gate_capability];
168 required_capabilities.extend(row.capabilities.clone());
169 required_capabilities.sort();
170 required_capabilities.dedup();
171 required_capabilities
172}
173
174fn require_session_capabilities(session: &McpSession, required: &[CapabilityName]) -> Result<()> {
175 for capability in required {
176 if !session
177 .granted_capabilities
178 .iter()
179 .any(|granted| granted == capability)
180 {
181 return Err(Error::CapabilityDenied {
182 capability: capability.clone(),
183 });
184 }
185 }
186 Ok(())
187}
188
189fn validate_input(cx: &mut Cx, input_shape: Option<&ShapeRef>, values: &[Value]) -> Result<()> {
190 let Some(shape) = input_shape else {
191 return Ok(());
192 };
193 let args_value = cx.factory().list(values.to_vec())?;
194 let matched = check_value_report(cx, shape, args_value)?;
195 if matched.accepted {
196 Ok(())
197 } else {
198 Err(Error::WrongShape {
199 expected: shape_id(shape),
200 diagnostics: matched.diagnostics,
201 })
202 }
203}
204
205fn call_local(
206 cx: &mut Cx,
207 row: &McpSurfaceCard,
208 request: &EvalRequest,
209 values: Vec<Value>,
210) -> Result<Value> {
211 let symbol = callable_symbol(&request.expr)?;
212 let value = cx.call_function(symbol, Args::new(values))?;
213 validate_output(cx, row.output_shape.as_ref(), value)
214}
215
216fn validate_output(cx: &mut Cx, output_shape: Option<&ShapeRef>, value: Value) -> Result<Value> {
217 let Some(shape) = output_shape else {
218 return Ok(value);
219 };
220 let matched = check_value_report(cx, shape, value.clone())?;
221 if matched.accepted {
222 Ok(value)
223 } else {
224 Err(Error::WrongShape {
225 expected: shape_id(shape),
226 diagnostics: matched.diagnostics,
227 })
228 }
229}
230
231fn callable_symbol(expr: &Expr) -> Result<&Symbol> {
232 let Expr::Call { operator, .. } = expr else {
233 return Err(Error::Eval("MCP tool request is not a call".to_owned()));
234 };
235 let Expr::Symbol(symbol) = operator.as_ref() else {
236 return Err(Error::Eval(
237 "MCP tool request operator is not a symbol".to_owned(),
238 ));
239 };
240 Ok(symbol)
241}
242
243fn shape_id(shape: &ShapeRef) -> ShapeId {
244 shape
245 .object()
246 .as_shape()
247 .and_then(|shape| shape.id())
248 .unwrap_or(ShapeId(0))
249}