11.6. Model Context Protocol (MCP)
This section describes how a Morloc program speaks
MCP, for readers writing a client, debugging
one, or serving a program without mim. For serving with mim, see
Serving. The examples use the smiles program from
Dependency management.
MCP support is part of the shared runtime; there is no separate build. Any
compiled program is served over MCP by running the runtime in mcp mode
against it:
$ morloc-nexus mcp ./smiles # or: morloc-nexus mcp smiles-build/manifest.json
MCP is a JSON-RPC 2.0 protocol. In this form it is spoken over the program’s standard input and output: the server reads requests on stdin and writes responses on stdout, and every other byte — pool output, log lines, diagnostics — goes to stderr so the protocol stream stays clean. This is the local transport: the client launches the server as a child process on the same machine.
The same command serves MCP over HTTP instead when given a port, which is the
way to reach one program from another machine without mim:
$ morloc-nexus mcp ./smiles --http-port 9000
It takes --http-host, --auth-token, and --allow-no-auth with the same
meaning as for the router (Daemons and the serving router). The router behind
mim start serves MCP over HTTP for several programs at once.
The stdio server launches the language pools and then waits for messages. It is normally started by an MCP client, but the transport is line-delimited JSON, so you can drive it by hand.
|
|
Each message is one complete JSON object on a single line. A
pretty-printed object split across lines is parsed as broken fragments, each
answered with a -32700 error. The transcripts below are indented only for
readability.
|
11.6.1. The handshake
A session opens with three messages: the client sends initialize, the server
replies with its capabilities, and the client confirms with an initialized
notification. Only then may tools be listed or called.
--> {"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"demo","version":"0"}}}
<-- {"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{"tools":{"listChanged":false}},
"serverInfo":{"name":"smiles","version":"<morloc version>"}}}
--> {"jsonrpc":"2.0","method":"notifications/initialized"}
The server answers with the protocol version the client asked for, and names
itself after the program with the Morloc version that compiled it. The
initialized notification carries no id and gets no reply. ping is
answered at any point; tools/list and tools/call are rejected with
-32600 until the handshake completes.
11.6.2. Inspecting the tool surface
Every exported function becomes one tool. The --mcp-tools flag prints the
payload tools/list returns, without starting a session:
$ ./smiles --mcp-tools | jq '.tools[] | {name, inputSchema}'
Each tool’s description comes from the function’s docstring, and each
argument’s Morloc type is rendered as a JSON Schema type (Int → integer,
Str → string, [a] → array, a record → object, ?a → a nullable
union). An argument’s own --' docstring becomes its property’s
description.
11.6.3. How arguments map to properties
MCP delivers arguments as a named object, so every argument needs a key. The key depends on the kind of argument:
| Morloc argument | MCP property name |
|---|---|
A positional argument |
The name given by |
An option ( |
The long name ( |
A flag ( |
The flag’s name, typed as a |
An unrolled record ( |
One property per field, keyed by the field name. |
A record passed whole ( |
One |
An unrolled |
Its |
A command with @render or @with output actions (Output actions) also
takes an optional _render property naming one of them; "raw", the default,
returns the command’s own value. A name given with @name may not start with
an underscore, so these generated keys never collide with an author’s.
11.6.4. Calling a tool
tools/call names the tool and supplies its arguments by key. The server turns
the named arguments back into a positional call, dispatches it through the
same machinery the CLI and daemon use, and returns the result.
--> {"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"mw","arguments":{"_1":"CCO"}}}
A scalar or list return is placed in a single text content block. A record
return also fills structuredContent and the tool’s outputSchema, so a client
that understands structured output gets the typed object as well as the text.
Omitted optional arguments take their declared defaults, and an omitted record field is filled from its default; the client supplies only what it overrides.
11.6.5. What is not exposed
Some functions cannot be served over a JSON channel, so they are left out of the tool list, with a note on stderr saying why. The other tools are unaffected.
| Excluded when the function… | …because |
|---|---|
reads from |
on the stdio transport, stdin is the JSON-RPC input stream. |
has an Arrow |
a |
has a stream argument or return ( |
a live handle cannot be sent as a JSON value. |
would publish two properties with the same key |
the arguments could not be told apart. |
|
|
Ordinary printing from a function — a print in a Python pool, a
std::cout in C++ — does not corrupt the protocol. The server moves its own
standard output aside before any pool starts, so stray writes land on stderr.
|
11.6.6. Errors
A malformed request is a JSON-RPC error. A function that runs and fails is a
normal result flagged with isError, so the agent can read the message and
react.
| Condition | Response |
|---|---|
Unparseable message |
JSON-RPC error |
A tool method before the handshake |
JSON-RPC error |
Unknown method |
JSON-RPC error |
Unknown tool, or missing, unexpected, or wrong-typed arguments |
JSON-RPC error |
The function raises ( |
A result with |
--> {"jsonrpc":"2.0","id":4,"method":"tools/call",
"params":{"name":"fireball","arguments":{}}}
<-- {"jsonrpc":"2.0","id":4,
"error":{"code":-32602,"message":"unknown tool 'fireball'"}}
11.6.7. Connecting a local client
A client that launches the server needs a command and its arguments: the
runtime in mcp mode against the program’s absolute manifest path. The
launcher prints that entry for you, as JSON on stdout:
$ ./smiles --mcp-config > smiles.mcp.json
It has this shape:
{
"mcpServers": {
"smiles": {
"command": "/absolute/path/to/morloc-nexus",
"args": ["mcp", "/absolute/path/to/smiles-build/manifest.json"]
}
}
}
The command is an absolute path because MCP clients launch servers with a
minimal PATH. For Claude Code, claude mcp add smiles — /absolute/path/to/morloc-nexus mcp /absolute/path/to/smiles-build/manifest.json
registers the same entry. The client launches the server, runs the handshake,
and lists the tools. When the client disconnects, closing stdin, the server
stops its pools and exits.
A client can launch the program only where Morloc is installed: on the same host for a native environment, or inside the same container. An agent anywhere else reaches the program over HTTP.
11.6.8. MCP over HTTP
Over HTTP, from mim start, the router, or mcp --http-port, the same
messages travel in the bodies of POST /mcp requests, using MCP’s Streamable
HTTP transport. Two things change:
-
Sessions. The reply to
initializecarries anMcp-Session-Idheader, and every later request must send it back. A request without it is400; an unknown or expired one is404, and the client re-initializes. A session expires after an hour.DELETE /mcpends one. -
Tool names. Because one endpoint can serve several programs, each tool is named
<module><command>:smilesmwrather thanmw.
11.6.9. Summary
| Aspect | Detail |
|---|---|
Local server |
|
Networked server |
|
Transport |
JSON-RPC 2.0 over stdio, or Streamable HTTP with |
Tools |
One per exported function, named |
Positional keys |
the |
Option and flag keys |
the long (or short) option name |
Return |
|
Static preview |
|