ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Simple.pm
Revision: 1.11
Committed: Mon Oct 12 05:04:00 2026 UTC (4 weeks, 2 days ago) by root
Branch: MAIN
Changes since 1.10: +48 -1 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     examples. The common MCP options are documented automatically and their
129     descriptions are shared by all callers. Application option descriptions
130     are explicit; a C<Getopt::Long> specification does not contain enough
131     information to write one.
132    
133     Other settings in that hash, such as C<path>, C<auth> or C<url_scheme>,
134     are passed through to C<mcp_start>. Without C<usage>, a default synopsis
135     is generated. Standard C<--help> prints the expanded help and exits;
136     a parsing error dies with the shorter usage synopsis.
137     C<--print-url> resolves the same name, bind, tokens and path as
138 root 1.8 startup, prints the first accepted token in the URL and exits B<without>
139     starting a listener. It requires a persistently stored master secret for
140     derived credentials and cannot predict a port requested as zero.
141    
142     All other option specifications and remaining C<@ARGV> follow the caller's
143     normal Getopt::Long configuration, including C<require_order> if selected.
144     C<--stdio> is understood by C<mcp_start> and skips HTTP configuration.
145     The helper deliberately does not enforce application-specific positional
146     arguments; C<--print-url> should not have to supply a workdir just to
147     print an address. It is a parser, not a second command language.
148    
149 root 1.1 =item mcp_start %config
150    
151     Starts and runs the usual server. When
152     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL> is C<stdio>, uses C<mcp_stdio>.
153 root 1.4 Otherwise starts HTTP without printing its URL. HTTP-capable executables
154     provide C<--print-url> to retrieve the URL explicitly.
155 root 1.1
156 root 1.3 The HTTP defaults are IPv4 loopback, a stable name-derived port in
157     16384..32767, and C</> as the endpoint path.
158 root 1.7 Without explicit C<tokens>, one server-specific token is derived from
159     the private global secret. The selected token authenticates either its URL
160     path or an
161 root 1.5 C<Authorization: Bearer TOKEN> header at the clean URL returned by
162 root 1.6 C<< $server->url (0) >>. The corresponding executable's C<--print-url>
163     option prints the first accepted token in its URL.
164 root 1.1
165 root 1.10 C<name> selects the C<[instance NAME]> configuration section and credential.
166 root 1.3 It defaults to the program's basename.
167 root 1.5 Named settings C<bind>, C<path>, C<url_host>, C<url_scheme> and C<tokens>
168 root 1.7 are loaded from the global configuration. Explicit C<tokens> (an ordered,
169     nonempty array reference of URL-safe strings) replaces the configured list
170     and the derived default. Every token works in the URL and bearer header.
171 root 1.3 An explicit C<bind> overrides the configured bind address. Other arguments
172 root 1.7 override these settings.
173 root 1.1
174     The global secret is generated automatically when needed; see
175     L<AnyEvent::MCP/GLOBAL CONFIGURATION>. No configuration is loaded for stdio.
176 root 1.4 HTTP startup neither prints to stdout nor changes its buffering.
177 root 1.7 C<mcp_start> does not parse C<@ARGV>; applications parse their own options.
178 root 1.8 The bundled executables use C<mcp_getoptions>, which handles shared
179     options and URL discovery before starting the event loop.
180 root 1.1
181     =item mcp_stdio %config
182    
183     Creates and runs a server over stdin/stdout. Arguments override the collected
184     configuration. Stdout buffering is disabled. On stdin EOF, pending replies
185     finish and stdout drains before
186     this function returns. Stream errors close the connection and cancel pending
187     requests.
188    
189     =item mcp_http %config
190    
191     Creates and runs an HTTP server. For example:
192    
193     mcp_http
194     host => "127.0.0.1",
195     port => 31740,
196 root 1.7 path => "/mcp/",
197     tokens => ["your-long-random-secret"];
198 root 1.1
199     Arguments override the collected configuration. Unlike C<mcp_start>, this
200 root 1.8 function does not derive or supply authentication. B<Always> pass C<tokens>,
201     or explicitly pass C<auth =E<gt> "none"> when a trusted embedding layer
202     enforces access. Omitting both is an error, not an unauthenticated default.
203     It does not print a URL; use
204 root 1.7 C<on_start> for application-specific startup output. Neither function
205     implements OAuth or Basic authentication. See
206     L<AnyEvent::MCP::Server::HTTP/AUTHENTICATION> for details.
207 root 1.1
208     All three starters return when the server is closed or on C<SIGINT> or
209     C<SIGTERM>. Signal shutdown closes connections and cancels outstanding
210     requests. The signal watchers belong to the blocking call.
211    
212     =back
213    
214     =cut
215    
216     package AnyEvent::MCP::Server::Simple;
217    
218     use common::sense;
219     use AnyEvent ();
220     use AnyEvent::MCP ();
221     use AnyEvent::MCP::Server::Toolset ();
222     use Exporter ();
223    
224     our @ISA = qw(Exporter);
225 root 1.8 our @EXPORT = qw(mcp_config mcp_tool mcp_getoptions mcp_start mcp_stdio mcp_http);
226 root 1.1
227     my %config;
228     my $toolset = AnyEvent::MCP::Server::Toolset->new;
229    
230     sub _run;
231    
232     sub mcp_config {
233     my %kv = @_;
234    
235     $kv{instructions} = AnyEvent::MCP::_clean_text $kv{instructions}
236     if exists $kv{instructions};
237    
238     @config{keys %kv} = values %kv;
239     }
240    
241     sub mcp_tool {
242     $toolset->tool (@_)
243     }
244    
245 root 1.9 sub _mcp_help_row {
246     my ($option, $description) = @_;
247     my $start = " $option";
248     my $margin = " " x 34;
249     my $line = length $start < 34 ? $start . substr ($margin, length $start) : "$start\n$margin";
250     my $length = 34;
251     my $out = "";
252    
253     for my $word (split /\s+/, $description) {
254     if ($length > 34 && $length + 1 + length $word > 79) {
255     $out .= "$line\n";
256     $line = $margin;
257     $length = 34;
258     }
259     if ($length > 34) {
260     $line .= " ";
261     ++$length;
262     }
263     $line .= $word;
264     $length += length $word;
265     }
266    
267     "$out$line\n"
268     }
269    
270     sub _mcp_help {
271     my ($usage, $description, $options, $option_heading, $examples) = @_;
272     $usage =~ s/\n*\z/\n/;
273     my $help = $usage;
274     $help .= "\n$description\n" if defined $description && length $description;
275    
276     $help .= "\nCommon options:\n";
277     my @common = (
278     ["--name NAME", "Select named configuration (default: executable name)"],
279     ["--bind ADDRESS[:PORT]", "Override the HTTP listen address or port"],
280     ["--token TOKEN", "Set an accepted token (repeatable; replaces defaults)"],
281     ["--stdio", "Serve MCP on stdin/stdout; other listeners may remain"],
282     ["--print-url", "Print authenticated HTTP URL and exit without listening"],
283     ["-h, --help", "Display this help and exit"],
284     );
285     $help .= _mcp_help_row (@$_) for @common;
286    
287     if ($options && @$options) {
288     $help .= "\n" . ($option_heading // "Application options:") . "\n";
289     $help .= _mcp_help_row (@$_) for @$options;
290     }
291     $help .= "\nExamples:\n$examples\n"
292     if defined $examples && length $examples;
293    
294     $help
295     }
296    
297 root 1.8 sub mcp_getoptions {
298     require Getopt::Long;
299     require AnyEvent::MCP::Config;
300    
301     my $settings = ref $_[0] eq "HASH" ? shift : {};
302     my %settings = %$settings;
303     my $usage = delete $settings{usage}
304 root 1.9 // "Usage: $0 [OPTION]... [ARG]...\n";
305     my $description = delete $settings{description};
306     my $options = delete $settings{options};
307     my $option_heading = delete $settings{option_heading};
308     my $examples = delete $settings{examples};
309 root 1.8 my %url = (%config, %settings);
310     delete @url{qw(name bind stdio)};
311    
312     my ($name, $bind, $stdio, $print_url, $help, @tokens);
313     Getopt::Long::GetOptions (
314     AnyEvent::MCP::Config::http_getopt (\$name, \$bind, \$print_url, \@tokens),
315     "stdio" => \$stdio,
316     "help|h" => \$help,
317     @_,
318     ) or die $usage;
319    
320     if ($help) {
321 root 1.9 print _mcp_help ($usage, $description, $options, $option_heading, $examples);
322 root 1.8 exit;
323     }
324    
325     $name = AnyEvent::MCP::Config::server_name ($name // $settings{name} // $config{name});
326     my %server = (
327     %settings,
328     name => $name,
329     (defined $bind ? (bind => $bind) : ()),
330     (@tokens ? (tokens => \@tokens) : ()),
331     ($stdio ? (stdio => 1) : ()),
332     );
333    
334     if ($print_url) {
335     my %options = (%url, (@tokens ? (tokens => \@tokens) : ()));
336     my $global = AnyEvent::MCP::Config->global;
337 root 1.10 $options{path} //= $global->{section}{"instance $name"}{path} // "/";
338 root 1.8 if (($options{auth} || "") eq "none") {
339     !exists $options{tokens} or die "Cannot combine auth => none with tokens\n";
340     }
341     my $selected_bind = defined $bind ? $bind : ($settings{bind} // $config{bind});
342     print $global->http_url ($name, $selected_bind, %options), "\n";
343     exit;
344     }
345    
346     %server
347     }
348    
349 root 1.1 sub mcp_start {
350 root 1.3 require AnyEvent::MCP::Config;
351 root 1.1
352     my %kv = (%config, @_);
353 root 1.3 my $name = AnyEvent::MCP::Config::server_name ($kv{name});
354 root 1.8 my $stdio = delete $kv{stdio};
355 root 1.1
356     $kv{name} = $name;
357     $kv{server_info} ||= { name => $name, version => "0" };
358    
359     return mcp_stdio (%kv)
360 root 1.8 if $stdio || $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio";
361 root 1.1
362     my $global = AnyEvent::MCP::Config->global;
363    
364 root 1.8 my %http = $global->http_settings ($name, delete $kv{bind});
365     delete $http{tokens} if ($kv{auth} || "") eq "none";
366    
367 root 1.1 %kv = (
368     path => "/",
369 root 1.8 %http,
370 root 1.1 %kv,
371     );
372    
373 root 1.4 mcp_http (%kv)
374 root 1.1 }
375    
376     sub mcp_stdio {
377     require AnyEvent::MCP::Server::JSONL;
378    
379 root 1.11 my %kv = (%config, @_);
380     my $idle_exit = delete $kv{idle_exit};
381 root 1.1 binmode STDIN;
382     binmode STDOUT;
383     my $selected = select STDOUT;
384     $| = 1;
385     select $selected;
386    
387 root 1.11 my $server = AnyEvent::MCP::Server::JSONL->new (toolsets => [$toolset], %kv);
388 root 1.1 $server->accept (\*STDIN, \*STDOUT);
389 root 1.11
390     if ($idle_exit) {
391     $idle_exit =~ /\A[0-9]+(?:\.[0-9]+)?\z/ && $idle_exit > 0
392     or die "invalid idle_exit seconds\n";
393     if ($ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio") {
394     my ($timer, $check);
395     $check = sub {
396     return if $server->{idle_exit_requested};
397     if (grep $_->{mcp_active}, values %{$server->{connections} || {}}) {
398     $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
399     } else {
400     $server->request_idle_exit;
401     }
402     };
403     my $schedule = sub {
404     $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
405     };
406     my $on_request = $server->{on_request};
407     $server->{on_request} = sub {
408     my ($server, $req) = @_;
409     if ($req->{method} ne "anyevent-mcp/shutdown") {
410     $server->{idle_exit_requested} = 0;
411     $schedule->();
412     my $reply = $req->{on_reply};
413     $req->{on_reply} = sub {
414     $reply->(@_);
415     $schedule->() unless $server->{_idle_shutting_down};
416     };
417     }
418     $on_request->(@_) if $on_request;
419     };
420     my $on_close = $server->{on_close};
421     $server->{on_close} = sub {
422     undef $timer;
423     $on_close->(@_) if $on_close;
424     };
425     $schedule->();
426     }
427     }
428 root 1.1 _run $server
429     }
430    
431     sub mcp_http {
432     require AnyEvent::MCP::Server::HTTP;
433    
434     my $server = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], %config, @_);
435     $server->listen;
436     _run $server
437     }
438    
439     sub _run {
440     my ($server) = @_;
441    
442     my $cv = AnyEvent->condvar;
443     my $stopped;
444     my $stop = sub { $cv->send unless $stopped++ };
445     my $on_close = $server->{on_close};
446     $server->{on_close} = sub {
447     $on_close->(@_) if $on_close;
448     $stop->();
449     };
450    
451     if ($server->isa ("AnyEvent::MCP::Server::JSONL")) {
452     my $on_connection_close = $server->{on_connection_close};
453     $server->{on_connection_close} = sub {
454     $on_connection_close->(@_) if $on_connection_close;
455     $stop->();
456     };
457     }
458    
459     my @signals = map {
460     AnyEvent->signal (signal => $_, cb => sub { $server->close })
461     } qw(INT TERM);
462    
463     my $ok = eval {
464     $server->event (start =>);
465     $cv->recv;
466     1
467     };
468     my $error = $@;
469     $server->close;
470     die $error unless $ok;
471     return
472     }
473    
474     1