Skip to content
Get Started

③ 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.

Mirobody is an MCP server, and it also has an MCP client side — the two are separate mechanisms:

DirectionWhat it is
Outbound to youYour discovered tools, served at /mcp
Inbound to MirobodyA 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.

Three routes are registered, and they differ only in how the caller is identified:

RouteIdentifies the caller byPurpose
POST /mcpAuthorization: Bearer <JWT>The plain endpoint. Anonymous for tools that need no identity.
POST /mcp/{secret}the secret in the pathA personal URL, for clients that cannot send a header.
POST /personal/mcpAuthorization: Bearer <JWT>Not MCP itself — it mints and returns your personal URL.
MethodBehaviour
initializeNegotiates the protocol version (see below) and reports a serverInfo name taken from HTTP_SERVER_NAME.
server/discoverThe 2026-07-28 stateless discovery method. Advertises exactly the same capabilities as initialize — they share one declaration so they cannot drift.
notifications/initializedAccepted, empty 200.
pingEmpty result.
tools/listEvery discovered tool, minus the ones this account has no data for. No credentials needed.
tools/callRuns one tool by name with an arguments object.
resources/list / resources/readThe 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/listAlways an empty list — the engine serves no MCP prompts.

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.

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.

Only tools that declare a user_info parameter need an identity. For those, tools/call tries three things in order before it gives up:

  1. The Authorization header the JWT is verified and its sub claim becomes the user_id
  2. 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
  3. 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.

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

Terminal window
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.

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:

config.localdb.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.

A personal URL embeds the identity in the path, for clients that cannot attach an Authorization header. Ask for yours with your JWT:

Terminal window
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:

FieldValue
authorization_endpoint/oauth/authorize
token_endpoint/oauth/token
registration_endpoint/oauth/register — dynamic client registration is open
introspection_endpoint/oauth/introspect
grant_types_supportedauthorization_code, refresh_token, client_credentials
code_challenge_methods_supportedS256 — PKCE only
scopes_supportedopenid, 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.

tools/list needs no credentials, so it is the quickest way to see what a server offers:

Terminal window
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:

Terminal window
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.