Synsemadocsv0.6.xENES

Servidores MCP

Servidores MCP

MCP (Model Context Protocol) le permite a un agente de IA — Claude, Cursor, cualquier cliente MCP — llamar tools y leer datos que vos exponés. Es JSON-RPC, y el serve + JSON nativos de Synsema lo vuelven un server de una sola ruta.

mcp-server.syn
-- Doc example: a minimal MCP server (Model Context Protocol) in Synsema. MCP is JSON-RPC;
-- the `serve` wiring is one route (shown in the prose). Here the handler is a pure function,
-- so it's doctestable — press Test.
intent: "doc example: MCP server handler"

task rpc(id, result)
    give {"jsonrpc": "2.0", "id": id, "result": result}

-- one tool: greet(name). Arguments come from an agent: check them, and answer a
-- wrong one with an error result the agent can read (never a crash).
task tool_greet(args)
    let name be get(args, "name", nothing)
    when not is_text(name)
        give {"content": [{"type": "text", "text": "greet needs a text argument 'name'"}], "isError": true}
    give {"content": [{"type": "text", "text": "Hello, " + name + "!"}]}

-- the JSON-RPC dispatcher: initialize / tools/list / tools/call.
-- A notification (no id, method "notifications/…") gets no answer: nothing.
task mcp(req)
    let id be get(req, "id", nothing)
    let method be get(req, "method", "")
    when starts_with(method, "notifications/")
        give nothing
    when method == "initialize"
        give rpc(id, {"protocolVersion": "2025-06-18", "serverInfo": {"name": "demo", "version": "1.0"}, "capabilities": {"tools": {}}})
    otherwise when method == "tools/list"
        give rpc(id, {"tools": [{"name": "greet", "description": "Greet someone", "inputSchema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}}]})
    otherwise when method == "tools/call"
        let p be get(req, "params", {})
        when get(p, "name", nothing) == "greet"
            give rpc(id, tool_greet(get(p, "arguments", {})))
        otherwise
            give rpc(id, {"content": [{"type": "text", "text": "unknown tool"}], "isError": true})
    otherwise
        give rpc(id, {})

test "initialize returns serverInfo + protocol version"
    let r be mcp({"id": 1, "method": "initialize", "params": {}})
    assert_eq((r["result"])["protocolVersion"], "2025-06-18")

test "tools/call greet returns text content"
    let r be mcp({"id": 2, "method": "tools/call", "params": {"name": "greet", "arguments": {"name": "Ada"}}})
    assert_eq((((r["result"])["content"])[0])["text"], "Hello, Ada!")

test "a missing or null argument is an error result, not a crash"
    let r be mcp({"id": 3, "method": "tools/call", "params": {"name": "greet", "arguments": {}}})
    assert_eq(r["result"]["isError"], true)
    let r2 be mcp(json_decode("{\"id\": 4, \"method\": \"tools/call\", \"params\": {\"name\": \"greet\", \"arguments\": {\"name\": null}}}"))
    assert_eq(r2["result"]["isError"], true)

test "a notification gets no answer"
    assert_eq(mcp({"jsonrpc": "2.0", "method": "notifications/initialized"}), nothing)

test "a big JSON-RPC id comes back exact"
    let r be mcp(json_decode("{\"jsonrpc\": \"2.0\", \"id\": 18446744073709551616, \"method\": \"tools/list\"}"))
    assert_eq(r["id"], 2 ** 64)
    assert(contains(json_encode(r), "\"id\": 18446744073709551616"))

Cablealo a serve (una ruta)§

require serve(8080)
serve on 8080
    route "POST /mcp"
        let out be mcp(json of request)
        give when out == nothing then respond("", "text/plain", 202) otherwise out

Eso es un servidor MCP completo: cualquier cliente MCP que hable HTTP se conecta a http://…/mcp. Los métodos centrales son initialize, tools/list y tools/call. Una notificación (notifications/initialized, sin id) no lleva respuesta JSON-RPC: el mcp del ejemplo da nothing y la ruta responde 202 Accepted con el body vacío, como pide el transporte HTTP de MCP.

json of request se parsea como json_decode (engine v0.6.30), así que un id JSON-RPC numérico de cualquier tamaño vuelve exacto en la respuesta.

Tools que hacen trabajo real§

Una tool es solo una task. Dale un cuerpo real — consultar una DB, calcular, llamar una API — y devolvé {content: [...]}:

task tool_orders(args)
    require db("./store.db")
    let rows be sql("SELECT id, total FROM orders WHERE customer = ?", [args["customer"]])
    give {"content": [{"type": "text", "text": json_encode(rows)}]}

Agregala a tools/list (con un inputSchema) y despachala en tools/call.

Los argumentos vienen de un agente — revisalos. args["customer"] es error si falta la clave, y un null de JSON llega como nothing, que el texto no concatena ("Hello, " + nothing es error desde engine v0.6.29). Leé con get(args, "name", nothing) y respondé a un argumento inválido con un resultado de error que el agente pueda leer — el tool_greet del ejemplo:

task tool_greet(args)
    let name be get(args, "name", nothing)
    when not is_text(name)
        give {"content": [{"type": "text", "text": "greet needs a text argument 'name'"}], "isError": true}
    give {"content": [{"type": "text", "text": "Hello, " + name + "!"}]}

json_encode(rows) en el resultado de una tool escribe solo datos: una task o un generador dentro del valor es un error que dice dónde está (json_encode: the value["f"] is a task, not data, engine v0.6.30).

Convertí una API vieja en MCP (un conversor)§

Envolvé una API REST existente como una tool MCP — el agente recibe una tool limpia y tipada; tu API queda intacta:

require net("api.legacy.com")
require secret("LEGACY_KEY")

task tool_weather(args)
    let r be http_get("https://api.legacy.com/v1/weather",
        {"x-api-key": secret("LEGACY_KEY")}, {"city": args["city"]})
    give {"content": [{"type": "text", "text": body of r}]}

Registrala, despachala — tu API legacy ahora es MCP-native, y el secreto queda redactado (ver Secretos).

Seguridad — ejecutar input no confiable en sandbox§

Si tu servidor MCP corre código o input que no confiás, poné un techo del host sobre la ejecución (--cap-set). El propio endpoint MCP de este sitio (/mcp) expone tools run_synsema/test_synsema que corren snippets en exactamente ese sandbox — así un agente puede verificar el Synsema que escribe, de forma segura.