Skip to main content

sim_lib_mcp/
exec.rs

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
14/// Returns the capability gating `tools/call` execution.
15pub fn mcp_tools_call_capability() -> CapabilityName {
16    CapabilityName::new("mcp.tools.call")
17}
18
19/// Returns the capability gating `resources/read` execution.
20pub fn mcp_resources_read_capability() -> CapabilityName {
21    CapabilityName::new("mcp.resources.read")
22}
23
24/// Returns the capability gating `prompts/get` execution.
25pub 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, &params.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, &params.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}