NAME
Claude::Agent::MCP - MCP (Model Context Protocol) server integration
SYNOPSIS
use Claude::Agent qw(query tool create_sdk_mcp_server);
use Claude::Agent::Options;
use IO::Async::Loop;
# Create custom tools that execute locally in your Perl process
my $calculator = tool(
'calculate',
'Perform basic arithmetic calculations',
{
type => 'object',
properties => {
a => { type => 'number', description => 'First operand' },
b => { type => 'number', description => 'Second operand' },
operation => { type => 'string', enum => [qw(add subtract multiply divide)] },
},
required => ['a', 'b', 'operation'],
},
sub {
my ($args) = @_;
my ($a, $b, $op) = @{$args}{qw(a b operation)};
my $result = $op eq 'add' ? $a + $b
: $op eq 'subtract' ? $a - $b
: $op eq 'multiply' ? $a * $b
: $op eq 'divide' ? ($b != 0 ? $a / $b : 'Error: division by zero')
: 'Error: unknown operation';
return { content => [{ type => 'text', text => "Result: $result" }] };
}
);
# Create an SDK MCP server with your tools
my $server = create_sdk_mcp_server(
name => 'math',
tools => [$calculator],
version => '1.0.0',
);
# Use the tools in a query
my $options = Claude::Agent::Options->new(
mcp_servers => { math => $server },
allowed_tools => ['mcp__math__calculate'],
permission_mode => 'bypassPermissions',
);
my $loop = IO::Async::Loop->new;
my $iter = query(
prompt => 'Calculate 15 multiplied by 7',
options => $options,
loop => $loop,
);
while (my $msg = $iter->next) {
if ($msg->isa('Claude::Agent::Message::Result')) {
print $msg->result, "\n";
last;
}
}
DESCRIPTION
Claude::Agent::MCP provides MCP (Model Context Protocol) server integration, allowing you to create custom tools that Claude can use during queries.
What are MCP Tools?
MCP tools extend Claude's capabilities beyond the built-in tools (Read, Write, Bash, etc.). Your custom tools can:
Query databases
Call external APIs
Access application state
Perform domain-specific calculations
Interface with any Perl module or resource
SDK vs External MCP Servers
SDK MCP Servers (recommended for most use cases):
Tool handlers run in your Perl process
Full access to your application's state and resources
Created with
create_sdk_mcp_server()Tools named:
mcp__<server>__<tool>
External MCP Servers (for existing MCP services):
Run as separate processes or remote services
Configured with StdioServer, SSEServer, or HTTPServer
Useful for integrating with third-party MCP servers
CREATING SDK TOOLS
Step 1: Define Your Tool
use Claude::Agent qw(tool);
my $lookup = tool(
'lookup_user', # Tool name
'Look up user info by ID', # Description for Claude
{ # JSON Schema for input
type => 'object',
properties => {
user_id => {
type => 'integer',
description => 'The user ID to look up',
},
},
required => ['user_id'],
},
sub { # Handler (runs locally)
my ($args) = @_;
my $user = MyApp::DB->find_user($args->{user_id});
return {
content => [{
type => 'text',
text => $user ? "Found: $user->{name}" : "User not found",
}],
};
}
);
Step 2: Create an MCP Server
use Claude::Agent qw(create_sdk_mcp_server);
my $server = create_sdk_mcp_server(
name => 'myapp', # Server name (used in tool naming)
tools => [$lookup, $other], # Array of tool definitions
version => '1.0.0', # Optional version
);
Step 3: Use in a Query
use Claude::Agent qw(query);
use Claude::Agent::Options;
my $options = Claude::Agent::Options->new(
mcp_servers => { myapp => $server },
allowed_tools => ['mcp__myapp__lookup_user'],
);
my $iter = query(
prompt => 'Look up user 42',
options => $options,
);
TOOL HANDLER DETAILS
Input
Handlers receive a single hashref with the validated input parameters:
sub handler {
my ($args) = @_;
# $args->{param1}, $args->{param2}, etc.
}
Output
Handlers must return a hashref with a content array:
return {
content => [
{ type => 'text', text => 'Result message' },
],
is_error => 0, # Optional, default false
};
Content can include multiple blocks:
return {
content => [
{ type => 'text', text => 'Primary result' },
{ type => 'text', text => 'Additional details' },
],
};
Error Handling
Return is_error => 1 for tool errors:
sub handler {
my ($args) = @_;
eval {
# ... do work ...
};
if ($@) {
return {
content => [{ type => 'text', text => "Error: $@" }],
is_error => 1,
};
}
return { content => [{ type => 'text', text => 'Success' }] };
}
Unhandled exceptions in handlers are caught and returned as errors automatically.
INPUT SCHEMA (JSON SCHEMA)
The input schema defines what parameters your tool accepts. Claude uses this to understand how to call your tool.
Basic Types
# String
{ type => 'string' }
{ type => 'string', enum => ['option1', 'option2'] }
# Number
{ type => 'number' }
{ type => 'integer' }
# Boolean
{ type => 'boolean' }
Objects
{
type => 'object',
properties => {
name => { type => 'string', description => 'User name' },
age => { type => 'integer', description => 'User age' },
},
required => ['name'],
}
Arrays
{
type => 'array',
items => { type => 'string' },
}
TOOL NAMING
SDK tools are automatically prefixed with the server name:
Server name: 'myapp'
Tool name: 'calculate'
Full name: 'mcp__myapp__calculate'
Use the full name in allowed_tools:
allowed_tools => ['mcp__myapp__calculate', 'mcp__myapp__lookup'],
Use $server->tool_names to get all full names:
my $names = $server->tool_names;
# ['mcp__myapp__calculate', 'mcp__myapp__lookup']
ARCHITECTURE
When you use an SDK MCP server, the following happens:
┌─────────────────┐
│ Your Perl App │
│ │
│ ┌───────────┐ │ Unix Socket ┌─────────────┐
│ │ SDKServer │◄─┼──────────────────►│ SDKRunner │
│ │ (handler) │ │ │ (MCP proto) │
│ └───────────┘ │ └──────┬──────┘
│ │ │ stdio
└─────────────────┘ │
▼
┌─────────────┐
│ Claude CLI │
└─────────────┘
1. Your app creates an SDKServer with tool handlers 2. SDKServer spawns SDKRunner as a child process 3. Claude CLI connects to SDKRunner via stdio (MCP protocol) 4. When Claude calls a tool, SDKRunner forwards via Unix socket 5. SDKServer executes your handler and returns the result 6. Result flows back through SDKRunner to Claude
This architecture allows handlers to run in your process with full access to application state, while still integrating with the MCP protocol.
EXTERNAL MCP SERVERS
For integrating with external MCP servers (not your own handlers):
Stdio Server
use Claude::Agent::MCP::StdioServer;
my $server = Claude::Agent::MCP::StdioServer->new(
command => '/path/to/mcp-server',
args => ['--some-flag'],
env => { API_KEY => $key },
);
SSE Server
use Claude::Agent::MCP::SSEServer;
my $server = Claude::Agent::MCP::SSEServer->new(
url => 'https://example.com/mcp/sse',
);
HTTP Server
use Claude::Agent::MCP::HTTPServer;
my $server = Claude::Agent::MCP::HTTPServer->new(
url => 'https://example.com/mcp',
);
DEBUGGING
Set the environment variable for debug output:
CLAUDE_AGENT_DEBUG=1 perl my_script.pl
This shows:
Tool call requests
Handler execution
Socket communication
MCP protocol messages
COMPLETE EXAMPLE
#!/usr/bin/env perl
use strict;
use warnings;
use Claude::Agent qw(query tool create_sdk_mcp_server);
use Claude::Agent::Options;
use IO::Async::Loop;
use DBI;
# Database connection (available to all handlers)
my $dbh = DBI->connect('dbi:SQLite:myapp.db');
# Tool to query users
my $find_user = tool(
'find_user',
'Find a user by email address',
{
type => 'object',
properties => {
email => { type => 'string', description => 'Email to search' },
},
required => ['email'],
},
sub {
my ($args) = @_;
my $user = $dbh->selectrow_hashref(
'SELECT * FROM users WHERE email = ?',
undef, $args->{email}
);
return {
content => [{
type => 'text',
text => $user
? "Found: $user->{name} (ID: $user->{id})"
: "No user found with email: $args->{email}",
}],
};
}
);
# Tool to count records
my $count_records = tool(
'count_records',
'Count records in a table',
{
type => 'object',
properties => {
table => { type => 'string', enum => [qw(users orders products)] },
},
required => ['table'],
},
sub {
my ($args) = @_;
my $table = $args->{table};
# Use quote_identifier for safe table name interpolation
my $quoted = $dbh->quote_identifier($table);
my ($count) = $dbh->selectrow_array("SELECT COUNT(*) FROM $quoted");
return {
content => [{ type => 'text', text => "Table $table has $count records" }],
};
}
);
# Create server
my $server = create_sdk_mcp_server(
name => 'database',
tools => [$find_user, $count_records],
);
# Run query
my $loop = IO::Async::Loop->new;
my $options = Claude::Agent::Options->new(
mcp_servers => { database => $server },
allowed_tools => $server->tool_names,
permission_mode => 'bypassPermissions',
);
my $iter = query(
prompt => 'How many users are in the database? Find user with email test@example.com',
options => $options,
loop => $loop,
);
while (my $msg = $iter->next) {
if ($msg->isa('Claude::Agent::Message::Assistant')) {
for my $block ($msg->content_blocks) {
print $block->text, "\n" if $block->isa('Claude::Agent::Content::Text');
}
}
elsif ($msg->isa('Claude::Agent::Message::Result')) {
print "\nFinal: ", $msg->result, "\n";
last;
}
}
SEE ALSO
Claude::Agent - Main SDK module
Claude::Agent::MCP::ToolDefinition - Tool definition class
Claude::Agent::MCP::Server - SDK MCP server class
Claude::Agent::Options - Query options including mcp_servers
AUTHOR
LNATION, <email at lnation.org>
LICENSE
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under The Artistic License 2.0 (GPL Compatible).