Skip to main content

sim_lib_mcp/
ops.rs

1use std::sync::Arc;
2
3use sim_kernel::{
4    Args, Callable, Cx, Error, Export, Expr, Object, ObjectCompat, Result, Symbol, Value,
5};
6use sim_shape::{AnyShape, ListShape, Shape, shape_value};
7
8use crate::McpRouter;
9use crate::methods::{
10    core, prompts as prompt_methods, resources as resource_methods, tools as tool_methods,
11};
12
13/// Selects which MCP method a [`McpFunction`] callable dispatches to.
14#[derive(Clone, Copy)]
15pub enum McpFunctionKind {
16    /// Route a full encoded MCP envelope.
17    Handle,
18    /// Run the `initialize` handshake.
19    Initialize,
20    /// List available tools.
21    Tools,
22    /// Invoke a tool (`tools/call`).
23    Call,
24    /// List available resources.
25    Resources,
26    /// Read a resource (`resources/read`).
27    Read,
28    /// List available prompts.
29    Prompts,
30    /// Fetch a prompt (`prompts/get`).
31    GetPrompt,
32    /// Provide the sampling runner object.
33    #[cfg(feature = "sampling")]
34    SamplingRunner,
35    /// Report server health.
36    Health,
37}
38
39impl McpFunctionKind {
40    /// Returns the runtime symbol naming this method.
41    pub fn symbol(self) -> Symbol {
42        match self {
43            Self::Handle => handle_symbol(),
44            Self::Initialize => initialize_symbol(),
45            Self::Tools => tools_symbol(),
46            Self::Call => call_symbol(),
47            Self::Resources => resources_symbol(),
48            Self::Read => read_symbol(),
49            Self::Prompts => prompts_symbol(),
50            Self::GetPrompt => get_prompt_symbol(),
51            #[cfg(feature = "sampling")]
52            Self::SamplingRunner => crate::sampling::mcp_sampling_runner_symbol(),
53            Self::Health => health_symbol(),
54        }
55    }
56}
57
58/// Runtime callable object that dispatches one MCP method.
59#[derive(Clone)]
60pub struct McpFunction {
61    kind: McpFunctionKind,
62}
63
64impl McpFunction {
65    /// Creates a function callable for `kind`.
66    pub fn new(kind: McpFunctionKind) -> Self {
67        Self { kind }
68    }
69
70    /// Returns the runtime symbol naming this function.
71    pub fn symbol(&self) -> Symbol {
72        self.kind.symbol()
73    }
74
75    /// Creates a shared [`McpFunction`] for `kind`.
76    pub fn value(kind: McpFunctionKind) -> Arc<Self> {
77        Arc::new(Self::new(kind))
78    }
79}
80
81impl Object for McpFunction {
82    fn display(&self, _cx: &mut Cx) -> Result<String> {
83        Ok(format!("#<function {}>", self.symbol()))
84    }
85
86    fn as_any(&self) -> &dyn std::any::Any {
87        self
88    }
89}
90
91impl ObjectCompat for McpFunction {
92    fn as_callable(&self) -> Option<&dyn Callable> {
93        Some(self)
94    }
95}
96
97impl Callable for McpFunction {
98    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
99        match self.kind {
100            McpFunctionKind::Handle => handle(cx, args),
101            McpFunctionKind::Initialize => initialize(cx, args),
102            McpFunctionKind::Tools => tools(cx, args),
103            McpFunctionKind::Call => call(cx, args),
104            McpFunctionKind::Resources => resources(cx, args),
105            McpFunctionKind::Read => read(cx, args),
106            McpFunctionKind::Prompts => prompts(cx, args),
107            McpFunctionKind::GetPrompt => get_prompt(cx, args),
108            #[cfg(feature = "sampling")]
109            McpFunctionKind::SamplingRunner => sampling_runner(cx, args),
110            McpFunctionKind::Health => health(cx, args),
111        }
112    }
113
114    fn browse_args_shape(&self, _cx: &mut Cx) -> Result<Option<sim_kernel::ShapeRef>> {
115        let shape: Arc<dyn Shape> = match self.kind {
116            McpFunctionKind::Handle
117            | McpFunctionKind::Call
118            | McpFunctionKind::Read
119            | McpFunctionKind::GetPrompt => Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
120            McpFunctionKind::Initialize
121            | McpFunctionKind::Tools
122            | McpFunctionKind::Resources
123            | McpFunctionKind::Prompts
124            | McpFunctionKind::Health => Arc::new(ListShape::new(Vec::new())),
125            #[cfg(feature = "sampling")]
126            McpFunctionKind::SamplingRunner => Arc::new(ListShape::new(Vec::new())),
127        };
128        Ok(Some(shape_value(
129            Symbol::qualified(self.symbol().to_string(), "args"),
130            shape,
131        )))
132    }
133
134    fn browse_result_shape(&self, _cx: &mut Cx) -> Result<Option<sim_kernel::ShapeRef>> {
135        Ok(Some(shape_value(
136            Symbol::qualified(self.symbol().to_string(), "result"),
137            Arc::new(AnyShape),
138        )))
139    }
140}
141
142/// Returns the export records for every MCP method function.
143pub fn mcp_exports() -> Vec<Export> {
144    [
145        handle_symbol(),
146        initialize_symbol(),
147        tools_symbol(),
148        call_symbol(),
149        resources_symbol(),
150        read_symbol(),
151        prompts_symbol(),
152        get_prompt_symbol(),
153        #[cfg(feature = "sampling")]
154        crate::sampling::mcp_sampling_runner_symbol(),
155        health_symbol(),
156    ]
157    .into_iter()
158    .map(|symbol| Export::Function {
159        symbol,
160        function_id: None,
161    })
162    .collect()
163}
164
165fn handle(cx: &mut Cx, args: Args) -> Result<Value> {
166    let expr = one_expr_arg(cx, args, "mcp/handle expects one decoded MCP envelope Expr")?;
167    let mut router = McpRouter::fixture();
168    match router.handle_expr(cx, expr)? {
169        Some(reply) => cx.factory().expr(reply),
170        None => cx.factory().nil(),
171    }
172}
173
174fn initialize(cx: &mut Cx, args: Args) -> Result<Value> {
175    no_args(args, "mcp/initialize expects no arguments")?;
176    let mut session = crate::McpSession::fixture();
177    cx.factory()
178        .expr(core::initialize(&mut session, Expr::Nil)?)
179}
180
181fn tools(cx: &mut Cx, args: Args) -> Result<Value> {
182    no_args(args, "mcp/tools expects no arguments")?;
183    let session = crate::McpSession::fixture();
184    let result = tool_methods::list(cx, &session)?;
185    cx.factory().expr(result)
186}
187
188fn call(cx: &mut Cx, args: Args) -> Result<Value> {
189    let params = one_expr_arg(cx, args, "mcp/call expects one tools/call params Expr")?;
190    let session = crate::McpSession::fixture();
191    let result = tool_methods::call(cx, &session, params)?;
192    cx.factory().expr(result)
193}
194
195fn resources(cx: &mut Cx, args: Args) -> Result<Value> {
196    no_args(args, "mcp/resources expects no arguments")?;
197    let session = crate::McpSession::fixture();
198    let result = resource_methods::list(cx, &session)?;
199    cx.factory().expr(result)
200}
201
202fn read(cx: &mut Cx, args: Args) -> Result<Value> {
203    let params = one_expr_arg(cx, args, "mcp/read expects one resources/read params Expr")?;
204    let session = crate::McpSession::fixture();
205    let result = resource_methods::read(cx, &session, params)?;
206    cx.factory().expr(result)
207}
208
209fn prompts(cx: &mut Cx, args: Args) -> Result<Value> {
210    no_args(args, "mcp/prompts expects no arguments")?;
211    let session = crate::McpSession::fixture();
212    let result = prompt_methods::list(cx, &session)?;
213    cx.factory().expr(result)
214}
215
216fn get_prompt(cx: &mut Cx, args: Args) -> Result<Value> {
217    let params = one_expr_arg(
218        cx,
219        args,
220        "mcp/get-prompt expects one prompts/get params Expr",
221    )?;
222    let session = crate::McpSession::fixture();
223    let result = prompt_methods::get(cx, &session, params)?;
224    cx.factory().expr(result)
225}
226
227#[cfg(feature = "sampling")]
228fn sampling_runner(cx: &mut Cx, args: Args) -> Result<Value> {
229    no_args(args, "mcp/sampling-runner expects no arguments")?;
230    crate::sampling::sampling_runner_value(
231        cx,
232        Arc::new(crate::sampling::McpSamplingRunner::fixture()),
233    )
234}
235
236fn health(cx: &mut Cx, args: Args) -> Result<Value> {
237    no_args(args, "mcp/health expects no arguments")?;
238    let session = crate::McpSession::fixture();
239    cx.factory().expr(core::health(&session))
240}
241
242fn one_expr_arg(cx: &mut Cx, args: Args, message: &'static str) -> Result<Expr> {
243    let mut values = args.into_vec();
244    if values.len() != 1 {
245        return Err(Error::Eval(message.to_owned()));
246    }
247    values.remove(0).object().as_expr(cx)
248}
249
250fn no_args(args: Args, message: &'static str) -> Result<()> {
251    if args.values().is_empty() {
252        Ok(())
253    } else {
254        Err(Error::Eval(message.to_owned()))
255    }
256}
257
258/// Returns the `mcp/handle` function symbol.
259pub fn handle_symbol() -> Symbol {
260    Symbol::qualified("mcp", "handle")
261}
262
263/// Returns the `mcp/initialize` function symbol.
264pub fn initialize_symbol() -> Symbol {
265    Symbol::qualified("mcp", "initialize")
266}
267
268/// Returns the `mcp/tools` function symbol.
269pub fn tools_symbol() -> Symbol {
270    Symbol::qualified("mcp", "tools")
271}
272
273/// Returns the `mcp/call` function symbol.
274pub fn call_symbol() -> Symbol {
275    Symbol::qualified("mcp", "call")
276}
277
278/// Returns the `mcp/resources` function symbol.
279pub fn resources_symbol() -> Symbol {
280    Symbol::qualified("mcp", "resources")
281}
282
283/// Returns the `mcp/read` function symbol.
284pub fn read_symbol() -> Symbol {
285    Symbol::qualified("mcp", "read")
286}
287
288/// Returns the `mcp/prompts` function symbol.
289pub fn prompts_symbol() -> Symbol {
290    Symbol::qualified("mcp", "prompts")
291}
292
293/// Returns the `mcp/get-prompt` function symbol.
294pub fn get_prompt_symbol() -> Symbol {
295    Symbol::qualified("mcp", "get-prompt")
296}
297
298/// Returns the `mcp/health` function symbol.
299pub fn health_symbol() -> Symbol {
300    Symbol::qualified("mcp", "health")
301}