Skip to main content

submilli_engine/codegen/function_emitter/
mcp.rs

1//! Codegen for `@mcp/<server>.<tool>(...)` calls.
2//!
3//! There is no per-tool Wasm import. Every call lowers to the single
4//! `submilli:mcp.call(server, tool, argsJson) -> unknown` host fn: the server and
5//! tool names (recovered from the mangled name) and the args (serialized via the
6//! object's `toJson` vtable slot) are pushed as three `(ref $string)`s, and the
7//! host returns guest objects directly. The typechecker wraps known-return
8//! MCP calls in a normal `Cast`, so this module never emits cast validation.
9
10use wasm_encoder::{BlockType, Instruction};
11
12use crate::ExprId;
13use crate::codegen::CodegenCtx;
14use crate::codegen::function_emitter::FunctionEmitter;
15use crate::codegen::function_emitter::expr::{emit_expr, emit_vtable_dispatch_on_object_stack};
16use crate::codegen::function_emitter::json::emit_inline_const_raw_string;
17
18pub(super) fn emit_mcp_call(
19    emitter: &mut FunctionEmitter,
20    ctx: &CodegenCtx,
21    server: &str,
22    tool: &str,
23    args: &[ExprId],
24) -> Result<(), crate::compiler_error::CompilerFailure> {
25    if args.len() > 1 {
26        return Err(crate::codegen::internal_failure(
27            "MCP calls require a single arguments object",
28        ));
29    }
30    // Push the three `(ref $string)` args: server, tool, argsJson. A zero-arg
31    // tool sends `{}`; otherwise the args object is serialized via its vtable
32    // `toJson` slot.
33    emit_inline_const_string(emitter, ctx, server)?;
34    emit_inline_const_string(emitter, ctx, tool)?;
35    if let Some(&arg) = args.first() {
36        emit_expr(emitter, ctx, arg)?;
37        let ty = &ctx
38            .ta
39            .try_expr(arg)
40            .map_err(crate::codegen::arena_failure)?
41            .ty;
42        let local = emitter.add_anonymous_local(ctx.symbols.value_type(ty)?)?;
43        emitter.instruction(Instruction::LocalTee(local));
44        super::expr::emit_is_undefined(emitter, ctx)?;
45        emitter.emit_if(BlockType::Result(
46            ctx.symbols.value_type(&crate::Type::String)?,
47        ));
48        emit_inline_const_string(emitter, ctx, "{}")?;
49        emitter.emit_else();
50        emitter.instruction(Instruction::LocalGet(local));
51        emit_vtable_dispatch_on_object_stack(emitter, ctx, 1);
52        emitter.emit_end();
53    } else {
54        emit_inline_const_string(emitter, ctx, "{}")?;
55    }
56
57    let call_idx = ctx
58        .symbols
59        .func_idx(&crate::mangle::host(
60            crate::runtime::MCP_MODULE_NAME,
61            "call",
62        ))
63        .ok_or_else(|| {
64            crate::codegen::internal_failure("submilli:mcp.call imported during codegen")
65        })?;
66    emitter.instruction(Instruction::Call(call_idx));
67
68    Ok(())
69}
70
71/// Push a real `(ref $string)` for an inline constant: the `string_vtable` over a
72/// freshly built `$rawString`.
73fn emit_inline_const_string(
74    emitter: &mut FunctionEmitter,
75    ctx: &CodegenCtx,
76    text: &str,
77) -> Result<(), crate::compiler_error::CompilerFailure> {
78    let intrinsics = ctx
79        .symbols
80        .intrinsic_type_indices()
81        .ok_or_else(|| crate::codegen::internal_failure("MCP intrinsic types are missing"))?;
82    let string_vtable_idx = ctx
83        .symbols
84        .prelude_global_idx("string_vtable")
85        .ok_or_else(|| crate::codegen::internal_failure("MCP string vtable is missing"))?;
86    emitter.instruction(Instruction::GlobalGet(string_vtable_idx));
87    emit_inline_const_raw_string(emitter, ctx, text)?;
88    emitter.instruction(Instruction::I64Const(0));
89    emitter.instruction(Instruction::StructNew(intrinsics.string));
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::TypedAst;
97    use crate::codegen::invariant_tests::{assert_internal, with_context};
98    use crate::codegen::{SymbolTable, tests::mock_symbols_with_intrinsics};
99
100    #[test]
101    fn mcp_import_failures_are_compiler_failures() {
102        with_context(&TypedAst::new(), &mock_symbols_with_intrinsics(), |ctx| {
103            let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
104            assert_internal(emit_mcp_call(&mut emitter, ctx, "server", "tool", &[]).unwrap_err());
105            assert_internal(emit_inline_const_string(&mut emitter, ctx, "server").unwrap_err());
106        });
107        with_context(&TypedAst::new(), &SymbolTable::default(), |ctx| {
108            assert_internal(
109                emit_inline_const_string(
110                    &mut FunctionEmitter::new(ctx, &[]).unwrap(),
111                    ctx,
112                    "server",
113                )
114                .unwrap_err(),
115            );
116            let invalid = crate::ExprId(u32::MAX);
117            assert_internal(
118                emit_mcp_call(
119                    &mut FunctionEmitter::new(ctx, &[]).unwrap(),
120                    ctx,
121                    "server",
122                    "tool",
123                    &[invalid; 2],
124                )
125                .unwrap_err(),
126            );
127        });
128    }
129}