③ Answers
Mirobody MCP Server
The /mcp endpoint — local clients, remote HTTPS access, personal URLs and OAuth
Mirobody serves its tools at /mcp over JSON-RPC 2.0. It is the same registry the engine’s own agents read, so a client like Claude Desktop or
Cursor calls exactly the tools your agents call — including the
ones you added yourself.
The server listens on HTTP_HOST:HTTP_PORT, which is 0.0.0.0:18080 in the shipped
compose.yaml. Every URL on this page assumes that port.
Server and client in one
Section titled “Server and client in one”Mirobody is an MCP server, and it also has an MCP client side — the two are separate mechanisms:
| Direction | What it is |
|---|---|
| Outbound to you | Your discovered tools, served at /mcp |
| Inbound to Mirobody | A per-user registry of external MCP servers, fetched with tools/list and converted to function descriptors |
A user’s external servers are stored per account and managed over three JSON endpoints —
GET/POST /api/user/mcp to read the map, POST /api/user/mcp/set to add or replace one
entry, POST /api/user/mcp/delete to remove it. An entry is keyed by name:
{ "my-server": { "url": "https://example.com/mcp", "token": "…optional bearer token…", "enabled": true, "order": 0 }}The loader walks that map, skips entries with enabled: false or an empty url, POSTs
tools/list to each remaining one with Authorization: Bearer <token> (falling back to
the caller’s own JWT when no per-server token is set), and turns every returned tool into
a function descriptor built from its name, description and inputSchema.
The endpoints
Section titled “The endpoints”Three routes are registered, and they differ only in how the caller is identified:
| Route | Identifies the caller by | Purpose |
|---|---|---|
POST /mcp | Authorization: Bearer <JWT> | The plain endpoint. Anonymous for tools that need no identity. |
POST /mcp/{secret} | the secret in the path | A personal URL, for clients that cannot send a header. |
POST /personal/mcp | Authorization: Bearer <JWT> | Not MCP itself — it mints and returns your personal URL. |
Methods the server implements
Section titled “Methods the server implements”| Method | Behaviour |
|---|---|
initialize | Negotiates the protocol version (see below) and reports a serverInfo name taken from HTTP_SERVER_NAME. |
server/discover | The 2026-07-28 stateless discovery method. Advertises exactly the same capabilities as initialize — they share one declaration so they cannot drift. |
notifications/initialized | Accepted, empty 200. |
ping | Empty result. |
tools/list | Every discovered tool, minus the ones this account has no data for. No credentials needed. |
tools/call | Runs one tool by name with an arguments object. |
resources/list / resources/read | The resources found in MCP_RESOURCE_DIRS. On read, the server substitutes the current server URL and the caller’s token into the resource text. |
prompts/list | Always an empty list — the engine serves no MCP prompts. |
Protocol versions
Section titled “Protocol versions”The server speaks MCP 2026-07-28 — the current stateless revision: per-request _meta, server/discover, a required resultType, deterministic tool ordering, and no session id. Under that revision there is no handshake at all, so a client’s very first request may be tools/list.
It negotiates down for older clients, accepting 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. A version it does not know is answered with its newest, and an explicitly unsupported one returns the spec-defined -32022.
Data-gated listing
Section titled “Data-gated listing”tools/list is honest per account. Before listing, the server probes whether the resolved caller actually holds each kind of data: no health rows hides query_health_indicators, no genotype rows hides get_genetic_data. A tool that could only ever answer “you have no data” is not worth a schema in every client.
The probe fails open — a database hiccup hides nothing, because shrinking the tool surface of a user who does have data is the worse failure.
Anything else comes back as JSON-RPC -32601 (method not found). A body that will not
parse is -32700, a request with no method is -32600, and a tools/call with no
params, no tool name, or a tool name the server does not have is -32602.
Caller resolution
Section titled “Caller resolution”Only tools that declare a user_info parameter need an identity. For those,
tools/call tries three things in order before it gives up:
- The
Authorizationheader the JWT is verified and itssubclaim becomes theuser_id - The secret in the URL a temporary secret is looked up first (it may also carry a session and an agent name), then the permanent personal one
- Neither — ask the user to log in the call returns an authorization URL pointing at
/mcplogin, plus polling parameters
When the URL’s secret carries an agent name, tools/list is filtered through that
agent’s ALLOWED_TOOLS_{NAME} whitelist and then its DISALLOWED_TOOLS_{NAME}
blacklist. Note that this filtered listing is not the same code path as the plain
one: with neither key configured it comes back empty, so an agent-scoped URL wants at
least one of the two set.
Connect a local client
Section titled “Connect a local client”Claude Desktop and Cursor speak MCP over stdio, so bridge them to the HTTP endpoint with a proxy. Put this in your client’s MCP configuration file:
{ "mcpServers": { "mirobody_mcp": { "command": "npx", "args": [ "-y", "universal-mcp-proxy" ], "env": { "UMCP_ENDPOINT": "http://localhost:18080/mcp" } } }}Check the server answers
curl -sX POST http://localhost:18080/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'A JSON-RPC result with a tools array means the endpoint is reachable and the
registry loaded.
Add the configuration
Write the JSON above into the client’s MCP config file, then restart the client completely — most clients read that file only at startup.
Confirm the tools appear
Mirobody should show up in the client’s list of MCP servers, with the tool names from
tools/list under it.
Ask for something that needs a tool
“Search my health indicators for HbA1c.” The first call that needs an identity triggers the login flow described above; after that, every call resolves to the same account.
Remote access
Section titled “Remote access”http://localhost:18080/mcp only works for a client on the same machine. For a remote
client — a cloud deployment, a ChatGPT App, a colleague’s laptop — set MCP_PUBLIC_URL
to a publicly reachable HTTPS origin in your config.{env}.yaml:
MCP_PUBLIC_URL: 'https://yourdomain.com'The engine uses that value when it has to hand out an absolute URL to something outside the process, and prints it as the address to open when the server starts.
Personal MCP URLs
Section titled “Personal MCP URLs”A personal URL embeds the identity in the path, for clients that cannot attach an
Authorization header. Ask for yours with your JWT:
curl -sX POST http://localhost:18080/personal/mcp \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{}'# → {"code":0,"data":{"url":"http://localhost:18080/mcp/<secret>"}}The secret is 96 bytes of URL-safe randomness, cached in Redis for a year, and the same
URL comes back on later calls until it expires. Post {"user_id": "…"} instead of an
empty body to mint a URL scoped to another account — allowed only when that account
has shared chat access with you, which the server checks before it agrees.
The engine also mints short-lived variants internally: those expire in ten minutes and
carry a session id and an agent name, which is what narrows tools/list to one agent’s
tool set.
For clients that do a proper OAuth handshake, the server publishes one metadata document
at three well-known paths — /.well-known/oauth-authorization-server, the same path with
/mcp appended, and /.well-known/mcp-configuration. They return identical content and
are registered at the server root, so HTTP_URI_PREFIX does not apply to them.
The document advertises the endpoints and the shape of the flow:
| Field | Value |
|---|---|
authorization_endpoint | /oauth/authorize |
token_endpoint | /oauth/token |
registration_endpoint | /oauth/register — dynamic client registration is open |
introspection_endpoint | /oauth/introspect |
grant_types_supported | authorization_code, refresh_token, client_credentials |
code_challenge_methods_supported | S256 — PKCE only |
scopes_supported | openid, profile, email, offline_access, plus mcp:read, mcp:write, mcp:tools, mcp:admin, mcp:connect |
There is also an /oauth2/authorize alias and /oauth2/check_state/{state}, which is
how the polling loop from a device-style login checks whether the browser step finished.
Calling it with curl
Section titled “Calling it with curl”tools/list needs no credentials, so it is the quickest way to see what a server offers:
curl -sX POST http://localhost:18080/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'tools/call on a tool that declares user_info needs an identity — a bearer JWT here,
or a personal URL instead of the plain path:
curl -sX POST http://localhost:18080/mcp \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "query_health_indicators", "arguments": { "keywords": ["HbA1c", "Hemoglobin A1c"], "aggregate": "stats" } } }'The result carries a content array with the JSON-encoded tool output, an isError flag
taken from the tool’s own success field, and — when the output is structured — a
structuredContent copy.
Next steps
Section titled “Next steps”What tools/list will show you
Put your own tool on this endpoint
How agents, tools and skills fit together
MCP_PUBLIC_URL, HTTP_PORT and the config layers