AnyEvent-MCP
view release on metacpan or search on metacpan
lib/AnyEvent/MCP/Server/Simple.pm view on Meta::CPAN
my $client = AnyEvent::MCP::Client::JSONL->new (
command => [$^X, "./echo-mcp", "--stdio"],
);
my $cv = AE::cv;
$client->call (echo => { text => "hello" }, sub {
$cv->send (@_);
});
my ($success, $result) = $cv->recv;
$success
or die "$result->{message}\n";
!$result->{isError}
or die "$result->{content}[0]{text}\n";
print $result->{content}[0]{text}, "\n";
Run C<perl try-echo>. It prints:
hello
The JSONL client starts the server, connects and negotiates the protocol on
first use. C<$^X> chooses the same Perl interpreter as the test client.
The condition variable keeps the event loop running until the reply arrives.
Keep C<$client> alive while its operations are outstanding.
There are two success checks. C<$success> means the JSON-RPC operation
completed successfully; a tool can still return C<isError> in that result.
This client expects our echo tool's text result. A general client must handle
other MCP content representations too.
To use HTTP, start the server in one terminal:
perl echo-mcp --bind :31742
and retrieve its URL in another:
perl echo-mcp --bind :31742 --print-url
Configure that URL in your MCP client and ask it to call echo with text
C<hello>. Both commands use instance C<echo-demo>, unless you override it
with C<--name>. Repeat the same listener overrides when printing the URL.
C<--print-url> prints the address and exits; it does not start a server.
For the small Perl client above, replace the JSONL import and constructor
with:
use AnyEvent::MCP::Client::HTTP;
my $client = AnyEvent::MCP::Client::HTTP->new (
url => $ARGV[0],
);
Pass the printed URL as its first command-line argument.
The rest of the call and result handling stays the same.
C<mcp_start> supplies named HTTP settings and a token derived from the
private global secret. Stdio needs no HTTP configuration. C<name> selects
local settings and credentials; C<server_name> identifies the advertised
MCP implementation. The two need not be the same.
See L<AnyEvent::MCP::Intro/CONFIGURING YOUR SETUP>.
=head2 Add an application option
Suppose you want the server user to choose the maximum text length.
Replace the original C<mcp_getoptions> call with:
my $max_length = 4096;
my $options = mcp_getoptions (
{
name => "echo-demo",
usage => "Usage: $0 [OPTION]...\n",
description => "Return text supplied to the echo tool.",
options => [
["--max-length N", "Maximum text length in characters"],
],
},
"max-length=i" => \$max_length,
);
$max_length > 0
or die "--max-length must be positive\n";
Use C<$max_length> in both the schema's C<maxLength> and the callback's
length check. The server user can then start:
perl echo-mcp --stdio --max-length 1000
The option specification is passed to L<Getopt::Long>. The C<options>
entries supply human-readable help; they do not register options.
The later tool examples use the original 4096-character limit; use your
chosen limit in them too if you keep this option.
Common options such as C<--stdio>, C<--bind> and C<--help> remain available.
If your server also takes positional arguments, validate the remaining
C<@ARGV> after C<mcp_getoptions>. It handles C<--help> and C<--print-url>
before that validation, so the user can retrieve an HTTP URL without
supplying an application workdir or starting the application.
Settings affecting URL discovery, such as C<name> and C<path>, should be
available to C<mcp_getoptions> through its leading settings hash or an
earlier C<mcp_config> call. A setting supplied only after the parser exits
for C<--print-url> cannot affect that printed URL.
=head2 Choose a result representation
Our echo callback returns one text content item. Use C<reply> when the
representation matters: text, other content types, a larger result or a
particular MCP result shape.
For an ordinary result object, you can replace the reply with:
$req->result ({ text => $text, length => length $text });
C<result> returns that hash as C<structuredContent> and also serializes it
as JSON in one text content item. Clients consuming only text can still
see the data, but it is deliberately duplicated. Wrap arrays or scalars in
an object, or use C<reply> to choose a different representation.
lib/AnyEvent/MCP/Server/Simple.pm view on Meta::CPAN
};
};
The tool is advertised as C<delayed-echo>, because registration converts
underscores to hyphens. Ask the client to call it with text C<hello> and
seconds C<2>. The callback returns immediately; the timer supplies the reply
later. Other requests can run while it waits.
Keep watcher objects alive while they are needed. Here the cancellation
callback retains C<$timer>, and the timer's callback retains C<$req> and
C<$text>. Cancellation releases the timer. Replying finishes the request
and removes its cancellation callback, releasing the watcher as well.
No separate job registry is needed for this example.
Check C<finished> in late completion callbacks before replying.
Cancellation sets both C<finished> and C<cancelled> and sends no reply.
For subprocesses or network work, the cancellation callback should stop
that operation and release its resources instead of merely abandoning
the eventual reply.
Server catches exceptions during synchronous dispatch, not exceptions
from a later timer or I/O callback. If an asynchronous operation can fail,
handle that failure in its completion callback and report it with
C<error> or C<reply>, provided the request is still active.
C<progress> only sends when the caller supplied a progress token.
Delivery is best effort; the HTTP server in this distribution does not
deliver progress notifications. A client without progress display can
still receive the final result.
=head2 When to use the object interfaces
Simple owns one default Toolset and the blocking server lifetime.
That is useful for a small standalone program. If you already run an
event loop, want multiple transports or tool registries, or need custom
protocol dispatch, construct the objects directly.
L<AnyEvent::MCP::Server::Toolset> registers reusable tools.
L<AnyEvent::MCP::Server> handles the protocol.
L<AnyEvent::MCP::Server::HTTP> and L<AnyEvent::MCP::Server::JSONL> manage
connections. L<AnyEvent::MCP::Server::Request> describes request fields,
replies, progress and cancellation. The same tool callback model applies
at both levels.
=head1 FUNCTIONS
=over
=item mcp_config %config
Adds server configuration. Repeated calls extend or replace previously declared
values. C<instructions> is treated as human-readable text and normalized.
Server and transport options are passed to the underlying objects.
Common options include:
=over
=item name => $instance
Local instance name used to select configuration and credentials. Independent
of the name reported to MCP clients.
=item server_name => $name
Advertised MCP server implementation name. Defaults to C<AnyEvent::MCP>.
=item server_version => $version
Advertised application version, not the MCP protocol revision. Accepts
an ordinary Perl version number; Simple stringifies it for MCP.
Defaults to the AnyEvent::MCP library version.
=item server_info => { name => $name, version => $version }
Raw MCP server identity, passed through unchanged to the underlying
server. Cannot be combined with C<server_name> or C<server_version>.
=item instructions => $text
Human-readable guidance for the client, normalized when configured.
=item short_instructions => $text
Optional condensed guidance for finding this server's tools. Used as the
advertised MCP instructions when C<auto_instructions> is enabled.
=item auto_instructions => $boolean
Enabled by default. Exposes full instructions through the generated
C<meta-instructions> tool while advertising the short instructions if set.
When short or custom instructions are provided, append a brief hint pointing
to the full tool description; otherwise advertise full instructions as before.
Disable to advertise full instructions and omit the generated tool.
=item custom_instructions => $text
Instance-specific purpose prepended to advertised instructions. Can also be
set through C<--custom-instructions> or C<custom_instructions> in the program
or instance section of the global INI configuration.
=item on_request => $callback->($server, $req)
Synchronous hook before each dispatch attempt. For example, refresh
database state here. This does not replace dispatch or receive a
completion callback, and permission retries can invoke it again.
=item on_start => $callback->($server)
Called after the transport is ready, before entering the event loop.
Use it for startup information or installing watchers. Stdout carries
protocol messages in stdio mode; print diagnostics to stderr.
The callback must not block.
=item idle_exit => $seconds
Optional positive idle time for spawned stdio servers. With a multibroker
advertising C<anyevent-mcp/multibroker> version 1, Simple requests shutdown
once no MCP requests have been active for that long. The broker can ignore
the request. HTTP servers, ordinary clients and servers without this
option are unaffected.
lib/AnyEvent/MCP/Server/Simple.pm view on Meta::CPAN
The bundled executables use C<mcp_getoptions>, which handles shared
options and URL discovery before starting the event loop.
=item mcp_connect %config
Run a reverse-connecting JSONL server using C<connect =E<gt>
"http[s]://host:port/token/alias">. HTTPS verifies the server certificate
and hostname. It sends the authenticated extension hello only after the
transport is established, then serves MCP over the socket. Refused TCP
connections retry every second for at most sixty attempts, and unexpected
established disconnects start a new retry period. Other TCP setup and TLS
handshake errors are reported and stop the server.
Calls are cancelled rather than replayed.
This connection has no spawned-child lifecycle authority.
=item mcp_stdio %config
Creates and runs a server over stdin/stdout. Arguments override the collected
configuration. Stdout buffering is disabled. On stdin EOF, pending replies
finish and stdout drains before
this function returns. Stream errors close the connection and cancel pending
requests.
=item mcp_http %config
Creates and runs an HTTP server. For example:
mcp_http
host => "127.0.0.1",
port => 31740,
path => "/mcp/",
tokens => ["your-long-random-secret"];
Arguments override the collected configuration. Unlike C<mcp_start>, this
function does not derive or supply authentication. B<Always> pass C<tokens>,
or explicitly pass C<auth =E<gt> "none"> when a trusted embedding layer
enforces access. Omitting both is an error, not an unauthenticated default.
It does not print a URL; use
C<on_start> for application-specific startup output. Neither function
implements OAuth or Basic authentication. See
L<AnyEvent::MCP::Server::HTTP/AUTHENTICATION> for details.
All three starters return when the server is closed or on C<SIGINT> or
C<SIGTERM>. Signal shutdown closes connections and cancels outstanding
requests. The signal watchers belong to the blocking call.
=back
=head1 COMMON COMMAND-LINE OPTIONS
C<mcp_getoptions> parses these options for executables using
L<AnyEvent::MCP::Server::Simple>. The executable's own OPTIONS section
documents any additional switches or differences in shared-option behavior.
=over
=item --name NAME
Select the instance (default C<default>). NAME starts with a letter or
underscore, followed by letters, digits, underscores or hyphens.
Selects C<[instance NAME]> configuration, derived HTTP port and credentials;
does not change the advertised MCP implementation name.
Example: C<--name project>.
=item --bind ADDRESS[:PORT]
Override HTTP listening address and port. Defaults to C<127.0.0.1> and a
stable instance-derived port in 16384..32767. C<:31740> uses the loopback
address and port 31740; C<127.0.0.1:31740> specifies both.
Port zero requests an ephemeral port and is incompatible with
C<--print-url>. Ignored for stdio-only startup.
=item --tls-cert FILE
Enable opportunistic HTTPS using a PEM certificate chain. Without
C<--tls-key>, FILE must also contain the private key. The same listener
continues to accept plain HTTP.
=item --tls-key FILE
Private-key PEM file for C<--tls-cert>. If supplied alone, FILE must
contain both certificate chain and private key. Requires C<Net::SSLeay>.
=item --token TOKEN
Accept TOKEN for HTTP authentication. Repeat to accept multiple tokens.
Explicit tokens replace configured or master-secret-derived defaults.
Tokens work both as credential-bearing URL components and with the
C<Authorization: Bearer TOKEN> header.
Example: C<--token first --token second>.
=item -s, --set KEY=VALUE
Override one trusted configuration entry for the selected instance in this
process. Applied after C<[program NAME]> and C<[instance NAME]> defaults,
without changing the configuration file. Repeat to override multiple entries;
the last assignment to a key wins. Empty values suppress inherited settings,
and additional equals signs belong to the value. The supported keys depend
on the server (for example, C<executable.perl> and C<prepend-path> in
L<mcp-fs>). Example: C<-s executable.perl=/opt/perl/bin/perl>.
=item --stdio
Serve MCP JSONL on stdin/stdout rather than the default HTTP listener.
Applications with auxiliary listeners can keep those active. Cannot
combine explicitly with C<--connect>.
=item --connect URL
Reverse-connect to an upstream multibroker. For example:
C<http://host:port/TOKEN/alias> or C<https://host:port/TOKEN/alias>.
HTTPS requires successful certificate and hostname verification; it
never falls back to plaintext. Replaces the default MCP listener and
overrides implicit stdio selection. Cannot combine with explicit
C<--stdio>.
=item --custom-instructions TEXT
Override configured instance-purpose guidance describing when MCP
clients should use the server.
Example: C<--custom-instructions 'Use for build logs'>.
=item --auto-allow TOOL[,ACTION[,CAPABILITY]]
Add a process-local permission allow rule, repeatable. TOOL identifies
the permission-checked tool; ACTION and CAPABILITY narrow the match.
Missing fields and C<*> match anything.
Example: C<--auto-allow '*,read,workdir-read'>.
Rules run in command-line order before saved rules; they cannot grant
permissions to tools that do not check permissions.
See L<AnyEvent::MCP::Server::Permissions/auto_rules>.
=item --auto-deny TOOL[,ACTION[,CAPABILITY]]
Add a repeatable process-local deny rule with the same matching syntax
and evaluation order as C<--auto-allow>.
Example: C<--auto-deny '*,exec'>.
=item --quiet
Suppress routine tool activity and tool-failure logging. Warnings and
ordinary diagnostics remain; C<PERL_ANYEVENT_LOG> determines diagnostic
levels and destinations.
=item --print-url[=USER]
Print credential-bearing URLs and exit without starting a listener.
With managed users, prints each effective enabled user's URL, or only
the exact username USER. Comments identify scope-qualified principals
and the selected auxiliary connection user. With configured TLS, HTTPS
is printed before HTTP. Explicit tokens retain a single-token output
and cannot be combined with a user selector. Requires a predictable,
nonzero port and a persistent master secret for derived credentials.
Example: C<--name project --print-url=claude>.
=item --print-urls
Alias for C<--print-url>.
=item --tool-help
Print currently registered tool names, descriptions, argument types,
required fields and defaults; exit without starting the MCP listener.
Registered tools can depend on installed utilities and application setup.
=item --help
Print shared and application-specific command-line help; exit without
starting a server. Invalid options instead report usage.
=back
=cut
package AnyEvent::MCP::Server::Simple;
use common::sense;
use Exporter ();
use JSON::XS ();
use Scalar::Util qw(refaddr);
use AnyEvent ();
use AnyEvent::Log ();
use AnyEvent::MCP ();
use AnyEvent::MCP::Server::Toolset ();
our @ISA = qw(Exporter);
our @EXPORT = qw(mcp_config mcp_tool mcp_getoptions mcp_tool_help mcp_log mcp_log_request mcp_start mcp_stdio mcp_http mcp_connect);
my %config = (
auto_instructions => 1,
on_request => \&mcp_log_request,
on_connection_error => sub {
my ($server, $handle, $error) = @_;
my $info = $handle->{mcp_peer}{client_info};
my $name = ref $info eq "HASH" ? $info->{name} : undef;
my @details = (
"id=" . sprintf ("%x", refaddr $handle),
defined $handle->{mcp_remote} ? "remote=$handle->{mcp_remote}" : (),
defined $name && !ref $name ? "client=" . JSON::XS::encode_json (substr $name, 0, 80) : (),
);
AE::log trace => "MCP connection " . join (" ", @details) . ": $error";
},
);
my $toolset = AnyEvent::MCP::Server::Toolset->new;
my $quiet;
sub _run;
# operational messages go to the configured AnyEvent::Log destination,
# independently of the diagnostic verbosity filter.
sub mcp_log {
lib/AnyEvent/MCP/Server/Simple.pm view on Meta::CPAN
$set{$key} = $value;
},
"tool-help" => \my $tool_help,
"quiet" => \my $silent,
"help" => \my $help,
@_,
) or die $usage;
if ($help) {
print _mcp_help $usage, @help;
exit;
}
$name = AnyEvent::MCP::Config::server_name ($name // $settings{name} // $config{name});
if (%set) {
my $instance = AnyEvent::MCP::Config->global->{section}{"instance $name"} ||= {};
%$instance = (%$instance, %set);
}
_mcp_log_init ($name, $silent);
my %server = (
%settings,
name => $name,
(defined $bind ? (bind => $bind) : ()),
(defined $tls_cert ? (tls_cert => $tls_cert) : ()),
(defined $tls_key ? (tls_key => $tls_key) : ()),
(@tokens ? (tokens => \@tokens) : ()),
($stdio ? (stdio => 1) : ()),
(defined $connect ? (connect => $connect) : ()),
(defined $custom_instructions ? (custom_instructions => $custom_instructions) : ()),
(@auto_rules ? (auto_rules => \@auto_rules) : ()),
($tool_help ? (tool_help => 1) : ()),
);
if (defined $print_url) {
my %options = (
%url,
(defined $tls_cert ? (tls_cert => $tls_cert) : ()),
(defined $tls_key ? (tls_key => $tls_key) : ()),
(@tokens ? (tokens => \@tokens) : ()),
);
my $global = AnyEvent::MCP::Config->global;
$options{path} //= $global->{section}{"instance $name"}{path}
// $global->{section}{"program " . AnyEvent::MCP::Config::program_name ()}{path} // "/";
if (($options{auth} || "") eq "none") {
!exists $options{tokens} or die "Cannot combine auth => none with tokens\n";
}
my $selected_bind = defined $bind ? $bind : ($settings{bind} // $config{bind});
my %http = ($global->http_settings ($name, $selected_bind), %options);
my @schemes = defined $http{tls_cert} || defined $http{tls_key}
? ("https", "http") : ($http{url_scheme} || "http");
my $managed = ($http{auth} || "") ne "none"
&& !exists $options{tokens}
&& !exists $global->{section}{"instance $name"}{tokens}
&& !exists $global->{section}{"program " . AnyEvent::MCP::Config::program_name ()}{tokens};
if ($managed) {
my $saved = (ref $global)->new (file => $global->{file});
$saved->{section}{global}{secret} eq $global->{section}{global}{secret}
or die "MCP master secret is not persistently stored in $global->{file}\n";
my $credentials = $global->user_credentials ($name);
if (length $print_url) {
@$credentials = grep $_->[0] eq $print_url, @$credentials;
@$credentials or die "User '$print_url' is not enabled\n";
}
my $connect = $global->connect_user ($name);
for my $entry (@$credentials) {
my ($user, $principal, $token) = @$entry;
for my $scheme (@schemes) {
print $global->http_url ($name, $selected_bind, %options,
tokens => [$token], url_scheme => $scheme),
" # user $principal", ($user eq $connect ? " (connect)" : ""), "\n";
}
}
} else {
length $print_url
and die "Cannot select a managed user with explicit tokens or auth=none\n";
for my $scheme (@schemes) {
print $global->http_url ($name, $selected_bind, %options, url_scheme => $scheme), "\n";
}
}
exit;
}
\%server
}
sub _permission_options {
my %kv = @_;
if (my $rules = delete $kv{auto_rules}) {
require AnyEvent::MCP::Server::Permissions;
my $permissions = $kv{permissions} ||= AnyEvent::MCP::Server::Permissions->new;
$permissions->auto_rules ($rules);
}
%kv
}
sub _server_options {
my %kv = @_;
if (exists $kv{server_name} || exists $kv{server_version}) {
!exists $kv{server_info}
or die "Cannot combine server_info with server_name/server_version\n";
my $name = delete $kv{server_name};
my $version = delete $kv{server_version};
$kv{server_info} = {
name => defined $name ? $name : "AnyEvent::MCP",
version => defined $version ? "$version" : "$AnyEvent::MCP::VERSION",
};
}
%kv
}
sub mcp_start {
require AnyEvent::MCP::Config;
my %kv = (%config, @_);
mcp_tool_help (delete $kv{tool_help}, @{$kv{toolsets} || [$toolset]});
my $name = AnyEvent::MCP::Config::server_name ($kv{name});
my $config_file = AnyEvent::MCP::Config::_file ();
my $global = -e $config_file ? AnyEvent::MCP::Config->global : undef;
my %ini = $global ? (
%{$global->{section}{"program " . AnyEvent::MCP::Config::program_name ()} || {}},
( run in 1.336 second using v1.01-cache-2.11-cpan-3f962061493 )