MCP Protocol
On Sun, 04 Oct 2026, by @lucasdicioccio, 466 words, 26 code snippets, 1 links, 0images.
Generated from documentation/mcp.md, the repository is the canonical source and may be ahead of this page.
MCP Protocol
The Model Context Protocol (MCP) integration allows agents to connect to external servers that provide dynamic tool listings and execution capabilities.
Overview
MCP is a protocol for model context exchange that enables:
- Dynamic tool discovery: Servers advertise available tools at runtime
- Structured tool calls: JSON-RPC based communication
- Process isolation: MCP servers run in separate processes
- Standardized interface: Common protocol across different tool providers
┌─────────────┐ JSON-RPC ┌─────────────┐
│ Agent │<------------------->│ MCP Server │
│ Runtime │ (stdio/stdin) │ Process │
└─────────────┘ └─────────────┘
│ │
│ Tool descriptions │ Tool execution
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ LLM │ │ External │
│ (tool use) │ │ Systems │
└─────────────┘ └─────────────┘
Architecture
Client Architecture (System.Agents.MCP.Client)
-- Client runtime managing MCP server process
data Runtime = Runtime
{ procHandle :: ProcessHandle
, stdinHandle :: Handle
, stdoutHandle :: Handle
, toolsList :: TVar [ToolDescription]
, callResults :: TVar (Map CallId Value)
, serverName :: Text
}Server Architecture (System.Agents.MCP.Server)
The framework can also act as an MCP server, exposing agents as tools to other MCP clients:
multiAgentsServer ::
McpServerConfig ->
[AgentTree.Props] ->
IO ()MCP Server Configuration
Simple Binary Configuration
{
"mcpServers": [
{
"tag": "McpSimpleBinary",
"contents": {
"name": "filesystem",
"executable": "/usr/bin/mcp-server-filesystem",
"args": ["--root", "/home/user/projects"]
}
}
]
}Environment variables (env)
An MCP server can receive configuration, including secrets, through
environment variables. env maps a variable name to a binding value: a
literal, or a reference to an agent parameter (see
parameters-and-bindings.md). The variables are
added to the inherited environment (and win on a clash).
{
"tag": "McpSimpleBinary",
"contents": {
"name": "github",
"executable": "/usr/bin/mcp-server-github",
"args": [],
"env": {
"GITHUB_TOKEN": {"tag": "Param", "contents": "github_token"},
"GITHUB_HOST": {"tag": "Literal", "contents": "github.com"}
}
}
}The server starts once per agent tree, so a Param must have a value at load
time (process scope: --set, --params-file, or a default). A parameter with
no such value, such as a session-scope one, is a load error. The values are not
put on the command line, and the traced process description shows variable
names only, so a secret parameter does not leak through ps or traces.
Haskell Configuration Type
data McpServerDescription
= McpSimpleBinary McpSimpleBinaryConfiguration
data McpSimpleBinaryConfiguration = McpSimpleBinaryConfiguration
{ name :: Text -- Display name
, executable :: FilePath -- Path to server binary
, args :: [Text] -- Command line arguments
, env :: Maybe (Map Text BindingValue) -- Extra environment variables
}Protocol Flow
1. Server Initialization
Agent Runtime MCP Server
│ │
│── Spawn process ──────────────>│
│ (executable with args) │
│ │
│<─ Initialize request ──────────│
│ (protocol version, etc.) │
│ │
│── Initialize response ────────>│
│ (server capabilities) │
│ │
2. Tool Discovery
-- Request tools list
sendRequest :: Runtime -> Method -> Value -> IO CallId
-- Receive tool descriptions
receiveTools :: Runtime -> IO [ToolDescription]3. Tool Execution Loop
Agent Runtime MCP Server LLM
│ │ │
│<───────────────────────────────│<── Tool call│
│ (Tool call from server) │ request │
│ │ │
│── Execute tool locally ────────│ │
│ (if agent-as-tool) │ │
│ │ │
│── Tool result ────────────────>│ │
│ │ │
│<───────────────────────────────│─── Forward ─>│
│ │ result │
JSON-RPC Protocol
Message Format
data JsonRpcMessage
= JsonRpcRequest
{ jsonrpc :: Text
, method :: Text
, params :: Maybe Value
, id :: Maybe CallId
}
| JsonRpcResponse
{ jsonrpc :: Text
, result :: Maybe Value
, error :: Maybe JsonRpcError
, id :: CallId
}
| JsonRpcNotification
{ jsonrpc :: Text
, method :: Text
, params :: Maybe Value
}Tool List Method
Request:
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 1
}Response:
{
"jsonrpc": "2.0",
"result": {
"tools": [
{
"name": "read_file",
"description": "Read contents of a file",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file"
}
},
"required": ["path"]
}
}
]
},
"id": 1
}Tool Call Method
Request:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/home/user/README.md"
}
},
"id": 2
}Response:
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "# Project README\n\nThis is the content..."
}
],
"isError": false
},
"id": 2
}Client Runtime
Starting a Client
startMcpClient ::
Tracer IO Trace ->
McpSimpleBinaryConfiguration ->
IO (Either McpError Runtime)
startMcpClient tracer config = do
-- Spawn process
let procConfig = (proc (unpack config.executable)
(map unpack config.args))
{ std_in = CreatePipe
, std_out = CreatePipe
, std_err = Inherit
}
(stdinH, stdoutH, _, procH) <- createProcess_ config.name procConfig
-- Initialize communication
runtime <- Runtime procH stdinH stdoutH
<$> newTVarIO []
<*> newTVarIO Map.empty
<*> pure config.name
-- Query tools
tools <- queryTools runtime
atomically $ writeTVar (toolsList runtime) tools
return $ Right runtimeTool Execution
callTool ::
Runtime ->
Text -> -- Tool name
Value -> -- Arguments
IO (Either McpError Value)
callTool rt toolName args = do
callId <- nextCallId
-- Send call request
sendRequest rt "tools/call" $ object
[ "name" .= toolName
, "arguments" .= args
]
-- Wait for response
waitForResponse rt callIdServer Mode
The framework can expose agents as MCP servers:
Starting MCP Server Mode
agents-exe mcp-server --agent-file agent.jsonServer Capabilities
data McpServerConfig = McpServerConfig
{ serverName :: Text
, serverVersion :: Text
, supportedProtocols :: [ProtocolVersion]
}
defaultMcpServerConfig :: McpServerConfig
defaultMcpServerConfig = McpServerConfig
{ serverName = "agents-mcp-server"
, serverVersion = "1.0.0"
, supportedProtocols = ["2024-11-05"]
}Agent-as-Tool Mapping
Each loaded agent is exposed as an MCP tool:
agentToMcpTool :: AgentTree.AgentTree -> ToolDescription
agentToMcpTool tree = ToolDescription
{ toolName = agentSlug tree
, toolDescription = agentAnnounce tree
, toolInputSchema = object
[ "type" .= ("object" :: Text)
, "properties" .= object
[ "prompt" .= object
[ "type" .= ("string" :: Text)
, "description" .= ("Prompt to send to the agent" :: Text)
]
]
, "required" .= (["prompt"] :: [Text])
]
}Error Handling
MCP Errors
data McpError
= ProcessStartError Text
| ProtocolError Text
| ToolNotFound Text
| ToolCallError Text
| ParseError Text
| TimeoutErrorError Response Format
{
"jsonrpc": "2.0",
"error": {
"code": -32600,
"message": "Invalid Request",
"data": "Additional error details"
},
"id": null
}Tracing
MCP operations are traced for debugging:
data Trace
= McpClientClientTrace ClientTrace
| McpClientRunTrace RunTrace
| McpClientLoopTrace LoopTrace
data ClientTrace
= SendingRequest CallId Method Value
| ReceivedResponse CallId Value
| ReceivedError CallId JsonRpcError
data RunTrace
= RunCommandStart Text
| RunCommandStopped Text ExitCode
| RunBufferMoved ByteString ByteString
data LoopTrace
= ToolsRefreshed [ToolDescription]
| StartToolCall Text Value
| EndToolCall Text Value Value
| ExitingToolCallLoopBest Practices
Server Implementation
- Idempotency: Tools should be safe to call multiple times
- Timeouts: Set reasonable timeouts for operations
- Validation: Validate all inputs before execution
- Error messages: Return clear, actionable error messages
- Resource cleanup: Properly cleanup on process exit
Client Usage
- Reconnect logic: Handle server crashes gracefully
- Tool caching: Cache tool lists but refresh periodically
- Concurrent calls: Be aware of server concurrency limits
- Input validation: Validate arguments before sending
Example: Filesystem MCP Server
{
"tag": "McpSimpleBinary",
"contents": {
"name": "filesystem",
"executable": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/docs"]
}
}Example: Custom MCP Server
A minimal MCP server in Python:
#!/usr/bin/env python3
import json
import sys
def send_message(msg):
print(json.dumps(msg))
sys.stdout.flush()
def handle_request(request):
method = request.get("method")
if method == "initialize":
return {
"protocolVersion": "2024-11-05",
"capabilities": {},
"serverInfo": {"name": "example", "version": "1.0.0"}
}
elif method == "tools/list":
return {
"tools": [
{
"name": "echo",
"description": "Echo back the input",
"inputSchema": {
"type": "object",
"properties": {
"message": {"type": "string"}
},
"required": ["message"]
}
}
]
}
elif method == "tools/call":
params = request.get("params", {})
name = params.get("name")
args = params.get("arguments", {})
if name == "echo":
return {
"content": [{"type": "text", "text": args.get("message", "")}],
"isError": False
}
return None
def main():
while True:
line = sys.stdin.readline()
if not line:
break
request = json.loads(line)
result = handle_request(request)
if "id" in request:
send_message({
"jsonrpc": "2.0",
"result": result,
"id": request["id"]
})
if __name__ == "__main__":
main()Debugging
Enable MCP tracing with verbose logging:
agents-exe run --agent-file agent.json --log-http http://localhost:8080/logView MCP communications in the trace output:
{
"e": {
"server": "filesystem",
"val": {"x": "tool-call-start", "name": "read_file"}
}
}