Rand Stats

MCP::Server

zef:apogee

Actions Status

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

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:

Tool Groups

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

LLM Tool Bridge

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

Protocol

Implements MCP protocol version 2025-11-25 over stdio:

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