
MCP::Server
A framework for building MCP (Model Context Protocol) servers in Raku. Register tools, resources, and prompts — the framework handles the JSON-RPC 2.0 protocol, initialization handshake, and message dispatch.
Synopsis
use MCP::Server;
my $server = MCP::Server.new(:name<my-tools>, :version<1.0>);
$server.tool: 'greet',
description => 'Greet someone by name',
params => {
name => { type => 'string', description => 'Name to greet', required => True },
},
handler => -> :%args { "Hello, {%args<name>}!" };
$server.run; # Listens on stdin/stdout
Use with Claude Code:
{
"mcpServers": {
"my-tools": {
"command": "raku",
"args": ["-I", "lib", "examples/my-server.raku"]
}
}
}
Tools are functions the LLM can call. Define parameters with types and descriptions — the framework generates JSON Schema automatically.
$server.tool: 'search',
description => 'Search the web',
params => {
query => { type => 'string', description => 'Search query', required => True },
limit => { type => 'integer', description => 'Max results' },
},
handler => -> :%args {
# Return a string (wrapped as text content)
"Results for {%args<query>}..."
};
Tool handlers receive :%args and return:
Str — wrapped as [{type: "text", text: $result}]
List — passed through as content items
On exception — returned as isError: true content
Group related tools under a common prefix using / as separator:
$server.tool-group: 'file', -> $g {
$g.tool: 'read',
description => 'Read a file',
params => { path => { type => 'string', required => True } },
handler => -> :%args { %args<path>.IO.slurp };
$g.tool: 'list',
description => 'List a directory',
params => { path => { type => 'string', required => True } },
handler => -> :%args { %args<path>.IO.dir.join("\n") };
};
# Registers as: file/read, file/list
Tools registered with MCP::Server can be used with any OpenAI-compatible LLM API. Define tools once, use them both as MCP tools (for Claude Code) and as function-calling tools (for LLM API calls).
use MCP::Server;
use LLM::Chat::Backend::OpenAICommon;
use LLM::Chat::Backend::Settings;
use LLM::Chat::Conversation::Message;
# Define tools
my $server = MCP::Server.new(:name<my-tools>);
$server.tool: 'get_weather',
description => 'Get weather for a location',
params => { location => { type => 'string', required => True } },
handler => -> :%args {
my $proc = run 'curl', '-s', "https://wttr.in/{%args<location>}?format=3", :out;
$proc.out.slurp(:close).trim;
};
# Convert to OpenAI format and send to LLM
my @tools = $server.tools-for-llm;
my $backend = LLM::Chat::Backend::OpenAICommon.new(...);
my @messages = (Message.new(:role<user>, :content<What is the weather in London?>),);
my $resp = $backend.chat-completion(@messages, :@tools);
# If LLM wants to call a tool, execute it
if $resp.has-tool-calls {
my @results = $server.execute-tool-calls($resp.tool-calls);
# @results are {role=>"tool", tool_call_id=>"...", content=>"..."}
# Append to messages and call LLM again for final answer
}
tools-for-llm converts registered tools to the OpenAI function-calling format. execute-tool-calls routes the LLM's tool call requests to your registered handlers and returns results ready to send back.
Resources
Resources expose data the LLM can read.
$server.resource: 'config://app',
name => 'App Config',
description => 'Application configuration',
mime-type => 'application/json',
handler => -> :%args { '{"debug": true}' };
Prompts
Prompts are pre-defined templates for the user.
$server.prompt: 'review',
description => 'Code review prompt',
arguments => [
{ name => 'code', description => 'Code to review', required => True },
],
handler => -> :%args { "Please review:\n{%args<code>}" };
Examples
examples/echo-server.raku — Minimal echo and reverse tools
examples/weather-server.raku — Weather via wttr.in
examples/file-server.raku — File system tools using tool groups
examples/strawberry-server.raku — The tool LLMs wish they had
Protocol
Implements MCP protocol version 2025-11-25 over stdio:
JSON-RPC 2.0 message format
initialize / initialized handshake
tools/list, tools/call
resources/list, resources/read
prompts/list, prompts/get
ping
Logging via notifications/message
Full error handling with standard JSON-RPC error codes
Transport
Default transport is stdio (stdin/stdout). Custom transports can be created by implementing the MCP::Server::Transport role:
use MCP::Server::Transport;
class MyTransport does MCP::Server::Transport {
method read-message(--> Str) { ... }
method write-message(Str:D $msg) { ... }
}
$server.run(:transport(MyTransport.new));
Author
Matt Doughty
License
Artistic-2.0