ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Simple.pm
Revision: 1.12
Committed: Mon Oct 12 23:18:44 2026 UTC (4 weeks ago) by root
Branch: MAIN
Changes since 1.11: +17 -6 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Server::Simple - simple function interface for MCP servers
4    
5     =head1 SYNOPSIS
6    
7     use AnyEvent::MCP::Server::Simple;
8    
9     mcp_config
10     server_info => {
11     name => "example",
12     version => "0",
13     },
14     instructions => <<'EOF';
15     Use echo to return the supplied text.
16     EOF
17    
18     mcp_tool echo =>
19     "Return the supplied text.",
20     {
21     type => "object",
22     properties => { text => { type => "string" } },
23     required => ["text"],
24     },
25     sub {
26     my ($req, $args) = @_;
27    
28     $req->reply ({
29     content => [{ type => "text", text => $args->{text} }],
30     });
31     };
32    
33     mcp_start;
34    
35     =head1 DESCRIPTION
36    
37 root 1.2 For most MCP servers, registering a few tools and starting a transport is
38     enough. This module does just that: call C<mcp_tool> to register callbacks,
39     optionally configure the server with C<mcp_config>, then start it with
40     C<mcp_start>, C<mcp_stdio> or C<mcp_http>. Registration alone does not start
41     a server or an event loop.
42 root 1.1
43     Human-readable descriptions and instructions are stripped of surrounding
44     whitespace and common indentation when registered, so indented here-documents
45     can be used naturally.
46    
47     C<mcp_start>, C<mcp_stdio> and C<mcp_http> are alternative blocking entry
48 root 1.2 points for a self-contained server. They run AnyEvent until shutdown; you
49     do not start another event loop. If your application already owns the server
50     lifetime or needs several transports, use the object interfaces instead.
51    
52     Simple uses L<AnyEvent::MCP::Server> and a default Toolset internally; it is
53     not a second server implementation. Applications needing custom dispatch,
54     such as a multibroker, can use the Server API directly.
55 root 1.1
56     =head1 FUNCTIONS
57    
58     =over
59    
60     =item mcp_config %config
61    
62     Adds server configuration. Repeated calls extend or replace previously declared
63     values. C<instructions> is treated as human-readable text and normalized.
64    
65     C<server_info> contains the MCP server metadata, including C<name> and
66     C<version>. C<instructions> contains guidance for the client.
67    
68     C<on_request> is a synchronous hook called before dispatch:
69    
70     $callback->($server, $req)
71    
72     For example, an application can refresh database state before each request.
73     It does not replace dispatch or receive a completion callback. These options
74     belong to Server; Simple merely collects and passes them on.
75    
76     C<on_start> is C<$callback->($server)>, called after the transport is ready
77     and before entering the event loop. It can print application startup
78     information or install watchers. C<$server> is the underlying server object.
79     The callback must not block.
80    
81 root 1.11 C<idle_exit> is an optional positive idle time in seconds for spawned stdio
82     servers. With a multibroker advertising C<anyevent-mcp/multibroker> version 1,
83     Simple requests shutdown once no MCP requests have been active for that long.
84     The broker can ignore the request. HTTP servers, ordinary clients and servers
85     without this option are unaffected.
86    
87 root 1.1 =item mcp_tool $name, $description, $input_schema, $callback->($req, $args)
88    
89     Registers a tool named C<$name> with C<$description> and the JSON Schema
90     hash reference C<$input_schema>. C<$description> is normalized like
91     C<instructions>.
92    
93     C<$callback> receives an L<AnyEvent::MCP::Server::Request> object C<$req>
94     and its C<< $req->{args} >> hash reference as C<$args>, for convenience.
95    
96     Tools complete the request with C<< $req->reply ($result) >>, or use
97     C<< $req->result ($data) >> and C<< $req->error ($message) >> for convenience.
98     Request context is available directly in
99     the hash, for example C<< $req->{tool} >> and C<< $req->{server} >>.
100    
101 root 1.8 =item %server_options = mcp_getoptions ({ %settings }, @getopt_options)
102    
103     Parse standard server options C<--name>, C<--bind>, repeatable
104     C<--token>, C<--stdio>, C<--print-url> and C<--help> alongside any
105     application-specific L<Getopt::Long> option specifications:
106    
107     my $verbose;
108     my %options = mcp_getoptions (
109 root 1.9 {
110     usage => "Usage: $0 [OPTION]... WORKDIR\n",
111     description => "Provide example tools through MCP.",
112     option_heading => "Example options:",
113     options => [
114     ["--verbose", "Show additional diagnostics"],
115     ],
116     examples => " $0 --stdio ./project",
117     path => "/",
118     },
119 root 1.8 "verbose!" => \$verbose,
120     );
121     @ARGV == 1 or die "workdir required\n";
122     mcp_start %options;
123    
124 root 1.9 The optional leading hash defines GNU-style help: C<usage> is the synopsis,
125     C<description> is the introductory paragraph, C<options> is an array of
126     C<[option label, description]> pairs, C<option_heading> names their section
127     (default C<Application options:>), and C<examples> contains preformatted
128 root 1.12 examples. To split application options into multiple sections, include a
129     plain string heading among the option pairs; a heading at the start replaces
130     the default C<option_heading>, and each later heading starts a new section.
131     The common MCP options are documented automatically and their descriptions
132     are shared by all callers. Application option descriptions are explicit;
133     a C<Getopt::Long> specification does not contain enough information to
134     write one.
135 root 1.9
136     Other settings in that hash, such as C<path>, C<auth> or C<url_scheme>,
137     are passed through to C<mcp_start>. Without C<usage>, a default synopsis
138     is generated. Standard C<--help> prints the expanded help and exits;
139     a parsing error dies with the shorter usage synopsis.
140     C<--print-url> resolves the same name, bind, tokens and path as
141 root 1.8 startup, prints the first accepted token in the URL and exits B<without>
142     starting a listener. It requires a persistently stored master secret for
143     derived credentials and cannot predict a port requested as zero.
144    
145     All other option specifications and remaining C<@ARGV> follow the caller's
146     normal Getopt::Long configuration, including C<require_order> if selected.
147     C<--stdio> is understood by C<mcp_start> and skips HTTP configuration.
148     The helper deliberately does not enforce application-specific positional
149     arguments; C<--print-url> should not have to supply a workdir just to
150     print an address. It is a parser, not a second command language.
151    
152 root 1.1 =item mcp_start %config
153    
154     Starts and runs the usual server. When
155     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL> is C<stdio>, uses C<mcp_stdio>.
156 root 1.4 Otherwise starts HTTP without printing its URL. HTTP-capable executables
157     provide C<--print-url> to retrieve the URL explicitly.
158 root 1.1
159 root 1.3 The HTTP defaults are IPv4 loopback, a stable name-derived port in
160     16384..32767, and C</> as the endpoint path.
161 root 1.7 Without explicit C<tokens>, one server-specific token is derived from
162     the private global secret. The selected token authenticates either its URL
163     path or an
164 root 1.5 C<Authorization: Bearer TOKEN> header at the clean URL returned by
165 root 1.6 C<< $server->url (0) >>. The corresponding executable's C<--print-url>
166     option prints the first accepted token in its URL.
167 root 1.1
168 root 1.10 C<name> selects the C<[instance NAME]> configuration section and credential.
169 root 1.3 It defaults to the program's basename.
170 root 1.5 Named settings C<bind>, C<path>, C<url_host>, C<url_scheme> and C<tokens>
171 root 1.7 are loaded from the global configuration. Explicit C<tokens> (an ordered,
172     nonempty array reference of URL-safe strings) replaces the configured list
173     and the derived default. Every token works in the URL and bearer header.
174 root 1.3 An explicit C<bind> overrides the configured bind address. Other arguments
175 root 1.7 override these settings.
176 root 1.1
177     The global secret is generated automatically when needed; see
178     L<AnyEvent::MCP/GLOBAL CONFIGURATION>. No configuration is loaded for stdio.
179 root 1.4 HTTP startup neither prints to stdout nor changes its buffering.
180 root 1.7 C<mcp_start> does not parse C<@ARGV>; applications parse their own options.
181 root 1.8 The bundled executables use C<mcp_getoptions>, which handles shared
182     options and URL discovery before starting the event loop.
183 root 1.1
184     =item mcp_stdio %config
185    
186     Creates and runs a server over stdin/stdout. Arguments override the collected
187     configuration. Stdout buffering is disabled. On stdin EOF, pending replies
188     finish and stdout drains before
189     this function returns. Stream errors close the connection and cancel pending
190     requests.
191    
192     =item mcp_http %config
193    
194     Creates and runs an HTTP server. For example:
195    
196     mcp_http
197     host => "127.0.0.1",
198     port => 31740,
199 root 1.7 path => "/mcp/",
200     tokens => ["your-long-random-secret"];
201 root 1.1
202     Arguments override the collected configuration. Unlike C<mcp_start>, this
203 root 1.8 function does not derive or supply authentication. B<Always> pass C<tokens>,
204     or explicitly pass C<auth =E<gt> "none"> when a trusted embedding layer
205     enforces access. Omitting both is an error, not an unauthenticated default.
206     It does not print a URL; use
207 root 1.7 C<on_start> for application-specific startup output. Neither function
208     implements OAuth or Basic authentication. See
209     L<AnyEvent::MCP::Server::HTTP/AUTHENTICATION> for details.
210 root 1.1
211     All three starters return when the server is closed or on C<SIGINT> or
212     C<SIGTERM>. Signal shutdown closes connections and cancels outstanding
213     requests. The signal watchers belong to the blocking call.
214    
215     =back
216    
217     =cut
218    
219     package AnyEvent::MCP::Server::Simple;
220    
221     use common::sense;
222     use AnyEvent ();
223     use AnyEvent::MCP ();
224     use AnyEvent::MCP::Server::Toolset ();
225     use Exporter ();
226    
227     our @ISA = qw(Exporter);
228 root 1.8 our @EXPORT = qw(mcp_config mcp_tool mcp_getoptions mcp_start mcp_stdio mcp_http);
229 root 1.1
230     my %config;
231     my $toolset = AnyEvent::MCP::Server::Toolset->new;
232    
233     sub _run;
234    
235     sub mcp_config {
236     my %kv = @_;
237    
238     $kv{instructions} = AnyEvent::MCP::_clean_text $kv{instructions}
239     if exists $kv{instructions};
240    
241     @config{keys %kv} = values %kv;
242     }
243    
244     sub mcp_tool {
245     $toolset->tool (@_)
246     }
247    
248 root 1.9 sub _mcp_help_row {
249     my ($option, $description) = @_;
250     my $start = " $option";
251     my $margin = " " x 34;
252     my $line = length $start < 34 ? $start . substr ($margin, length $start) : "$start\n$margin";
253     my $length = 34;
254     my $out = "";
255    
256     for my $word (split /\s+/, $description) {
257     if ($length > 34 && $length + 1 + length $word > 79) {
258     $out .= "$line\n";
259     $line = $margin;
260     $length = 34;
261     }
262     if ($length > 34) {
263     $line .= " ";
264     ++$length;
265     }
266     $line .= $word;
267     $length += length $word;
268     }
269    
270     "$out$line\n"
271     }
272    
273     sub _mcp_help {
274     my ($usage, $description, $options, $option_heading, $examples) = @_;
275     $usage =~ s/\n*\z/\n/;
276     my $help = $usage;
277     $help .= "\n$description\n" if defined $description && length $description;
278    
279     $help .= "\nCommon options:\n";
280     my @common = (
281     ["--name NAME", "Select named configuration (default: executable name)"],
282     ["--bind ADDRESS[:PORT]", "Override the HTTP listen address or port"],
283     ["--token TOKEN", "Set an accepted token (repeatable; replaces defaults)"],
284     ["--stdio", "Serve MCP on stdin/stdout; other listeners may remain"],
285     ["--print-url", "Print authenticated HTTP URL and exit without listening"],
286     ["-h, --help", "Display this help and exit"],
287     );
288     $help .= _mcp_help_row (@$_) for @common;
289    
290     if ($options && @$options) {
291 root 1.12 $help .= "\n" . ($option_heading // "Application options:") . "\n"
292     if ref $options->[0];
293     for my $option (@$options) {
294     if (ref $option) {
295     $help .= _mcp_help_row (@$option);
296     } else {
297     $help .= "\n" unless $help =~ /\n\n\z/;
298     $help .= "$option\n";
299     }
300     }
301 root 1.9 }
302     $help .= "\nExamples:\n$examples\n"
303     if defined $examples && length $examples;
304    
305     $help
306     }
307    
308 root 1.8 sub mcp_getoptions {
309     require Getopt::Long;
310     require AnyEvent::MCP::Config;
311    
312     my $settings = ref $_[0] eq "HASH" ? shift : {};
313     my %settings = %$settings;
314     my $usage = delete $settings{usage}
315 root 1.9 // "Usage: $0 [OPTION]... [ARG]...\n";
316     my $description = delete $settings{description};
317     my $options = delete $settings{options};
318     my $option_heading = delete $settings{option_heading};
319     my $examples = delete $settings{examples};
320 root 1.8 my %url = (%config, %settings);
321     delete @url{qw(name bind stdio)};
322    
323     my ($name, $bind, $stdio, $print_url, $help, @tokens);
324     Getopt::Long::GetOptions (
325     AnyEvent::MCP::Config::http_getopt (\$name, \$bind, \$print_url, \@tokens),
326     "stdio" => \$stdio,
327     "help|h" => \$help,
328     @_,
329     ) or die $usage;
330    
331     if ($help) {
332 root 1.9 print _mcp_help ($usage, $description, $options, $option_heading, $examples);
333 root 1.8 exit;
334     }
335    
336     $name = AnyEvent::MCP::Config::server_name ($name // $settings{name} // $config{name});
337     my %server = (
338     %settings,
339     name => $name,
340     (defined $bind ? (bind => $bind) : ()),
341     (@tokens ? (tokens => \@tokens) : ()),
342     ($stdio ? (stdio => 1) : ()),
343     );
344    
345     if ($print_url) {
346     my %options = (%url, (@tokens ? (tokens => \@tokens) : ()));
347     my $global = AnyEvent::MCP::Config->global;
348 root 1.10 $options{path} //= $global->{section}{"instance $name"}{path} // "/";
349 root 1.8 if (($options{auth} || "") eq "none") {
350     !exists $options{tokens} or die "Cannot combine auth => none with tokens\n";
351     }
352     my $selected_bind = defined $bind ? $bind : ($settings{bind} // $config{bind});
353     print $global->http_url ($name, $selected_bind, %options), "\n";
354     exit;
355     }
356    
357     %server
358     }
359    
360 root 1.1 sub mcp_start {
361 root 1.3 require AnyEvent::MCP::Config;
362 root 1.1
363     my %kv = (%config, @_);
364 root 1.3 my $name = AnyEvent::MCP::Config::server_name ($kv{name});
365 root 1.8 my $stdio = delete $kv{stdio};
366 root 1.1
367     $kv{name} = $name;
368     $kv{server_info} ||= { name => $name, version => "0" };
369    
370     return mcp_stdio (%kv)
371 root 1.8 if $stdio || $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio";
372 root 1.1
373     my $global = AnyEvent::MCP::Config->global;
374    
375 root 1.8 my %http = $global->http_settings ($name, delete $kv{bind});
376     delete $http{tokens} if ($kv{auth} || "") eq "none";
377    
378 root 1.1 %kv = (
379     path => "/",
380 root 1.8 %http,
381 root 1.1 %kv,
382     );
383    
384 root 1.4 mcp_http (%kv)
385 root 1.1 }
386    
387     sub mcp_stdio {
388     require AnyEvent::MCP::Server::JSONL;
389    
390 root 1.11 my %kv = (%config, @_);
391     my $idle_exit = delete $kv{idle_exit};
392 root 1.1 binmode STDIN;
393     binmode STDOUT;
394     my $selected = select STDOUT;
395     $| = 1;
396     select $selected;
397    
398 root 1.11 my $server = AnyEvent::MCP::Server::JSONL->new (toolsets => [$toolset], %kv);
399 root 1.1 $server->accept (\*STDIN, \*STDOUT);
400 root 1.11
401     if ($idle_exit) {
402     $idle_exit =~ /\A[0-9]+(?:\.[0-9]+)?\z/ && $idle_exit > 0
403     or die "invalid idle_exit seconds\n";
404     if ($ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio") {
405     my ($timer, $check);
406     $check = sub {
407     return if $server->{idle_exit_requested};
408     if (grep $_->{mcp_active}, values %{$server->{connections} || {}}) {
409     $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
410     } else {
411     $server->request_idle_exit;
412     }
413     };
414     my $schedule = sub {
415     $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
416     };
417     my $on_request = $server->{on_request};
418     $server->{on_request} = sub {
419     my ($server, $req) = @_;
420     if ($req->{method} ne "anyevent-mcp/shutdown") {
421     $server->{idle_exit_requested} = 0;
422     $schedule->();
423     my $reply = $req->{on_reply};
424     $req->{on_reply} = sub {
425     $reply->(@_);
426     $schedule->() unless $server->{_idle_shutting_down};
427     };
428     }
429     $on_request->(@_) if $on_request;
430     };
431     my $on_close = $server->{on_close};
432     $server->{on_close} = sub {
433     undef $timer;
434     $on_close->(@_) if $on_close;
435     };
436     $schedule->();
437     }
438     }
439 root 1.1 _run $server
440     }
441    
442     sub mcp_http {
443     require AnyEvent::MCP::Server::HTTP;
444    
445     my $server = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], %config, @_);
446     $server->listen;
447     _run $server
448     }
449    
450     sub _run {
451     my ($server) = @_;
452    
453     my $cv = AnyEvent->condvar;
454     my $stopped;
455     my $stop = sub { $cv->send unless $stopped++ };
456     my $on_close = $server->{on_close};
457     $server->{on_close} = sub {
458     $on_close->(@_) if $on_close;
459     $stop->();
460     };
461    
462     if ($server->isa ("AnyEvent::MCP::Server::JSONL")) {
463     my $on_connection_close = $server->{on_connection_close};
464     $server->{on_connection_close} = sub {
465     $on_connection_close->(@_) if $on_connection_close;
466     $stop->();
467     };
468     }
469    
470     my @signals = map {
471     AnyEvent->signal (signal => $_, cb => sub { $server->close })
472     } qw(INT TERM);
473    
474     my $ok = eval {
475     $server->event (start =>);
476     $cv->recv;
477     1
478     };
479     my $error = $@;
480     $server->close;
481     die $error unless $ok;
482     return
483     }
484    
485     1