# Expose your program to any AI agent — an MCP server in one route

> MCP is JSON-RPC. In Synsema the whole server is a dispatcher task and one POST route. Tools are tasks with real bodies, secrets stay sealed, and untrusted input runs under a capability ceiling.

Published 2026-09-02 · https://synsema.org/blog/mcp-server-in-one-route


The Model Context Protocol is how Claude, Cursor and every other MCP client call tools you expose.
It is JSON-RPC over HTTP, so in a language with a native server and native JSON, an MCP server is a
task that dispatches three methods and a route that calls it.

## The whole server

```synsema
require serve(8080)

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

task tool_greet(args)
    give {"content": [{"type": "text", "text": "Hello, " + args["name"] + "!"}]}

task mcp(req)
    let id be req["id"]
    when req["method"] == "initialize"
        give rpc(id, {"protocolVersion": "2025-06-18", "serverInfo": {"name": "demo", "version": "1.0"}, "capabilities": {"tools": {}}})
    otherwise when req["method"] == "tools/list"
        give rpc(id, {"tools": [{"name": "greet", "description": "Greet someone", "inputSchema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}}]})
    otherwise when req["method"] == "tools/call"
        let p be req["params"]
        when p["name"] == "greet"
            give rpc(id, tool_greet(p["arguments"]))
        give rpc(id, {"content": [{"type": "text", "text": "unknown tool"}], "isError": true})
    give rpc(id, {})

serve on 8080
    route "POST /mcp"
        give mcp(json of request)
```

Point a client at `http://host/mcp` and `greet` is in its tool list. The dispatcher is a pure task,
so it is unit-tested with `synsema test` like any other.

## Tools that do real work

A tool is a task with a body. Query a database, call an API, compute — and return `content`:

```synsema
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)}]}
```

Wrapping a legacy REST API is the same shape with `http_get` inside — the agent gets a typed tool,
you keep the API untouched, and the API key travels as a sealed secret the model never sees.

## Untrusted input, bounded

If a tool runs code or input you do not trust, run that part under a host ceiling (`--cap-set`).
The Synsema docs site does exactly this: its `/mcp` endpoint offers `run_synsema` and `test_synsema`
tools that execute snippets in a sandbox, so an agent can verify the code it writes.

**Next step:** the [MCP page](https://synsema.dev/en/0.6.x/42-mcp) in the docs, and — to
distribute a tool with its permissions attached — [Lamps](/blog/lamps-capabilities-with-a-ceiling-for-ai-agents).

