ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Simple.pm
Revision: 1.13
Committed: Tue Oct 13 10:17:31 2026 UTC (4 weeks ago) by root
Branch: MAIN
Changes since 1.12: +14 -21 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 root 1.13
251 root 1.9 my $start = " $option";
252     my $margin = " " x 34;
253 root 1.13 my $out = length $start < 34 ? "" : "$start\n";
254     my $line = length $start < 34 ? $start . (substr $margin, length $start) : $margin;
255 root 1.9
256     for my $word (split /\s+/, $description) {
257 root 1.13 if (length $line > 34 && (length $line) + 1 + length $word > 79) {
258 root 1.9 $out .= "$line\n";
259     $line = $margin;
260     }
261 root 1.13
262     $line .= " " if length $line > 34;
263 root 1.9 $line .= $word;
264     }
265    
266     "$out$line\n"
267     }
268    
269     sub _mcp_help {
270     my ($usage, $description, $options, $option_heading, $examples) = @_;
271 root 1.13
272 root 1.9 $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 root 1.12 $help .= "\n" . ($option_heading // "Application options:") . "\n"
289     if ref $options->[0];
290     for my $option (@$options) {
291     if (ref $option) {
292     $help .= _mcp_help_row (@$option);
293     } else {
294     $help .= "\n" unless $help =~ /\n\n\z/;
295     $help .= "$option\n";
296     }
297     }
298 root 1.9 }
299     $help .= "\nExamples:\n$examples\n"
300     if defined $examples && length $examples;
301    
302     $help
303     }
304    
305 root 1.8 sub mcp_getoptions {
306     require Getopt::Long;
307     require AnyEvent::MCP::Config;
308    
309 root 1.13 my %settings = %{ref $_[0] eq "HASH" ? shift : {}};
310 root 1.8 my $usage = delete $settings{usage}
311 root 1.9 // "Usage: $0 [OPTION]... [ARG]...\n";
312 root 1.13 my @help = delete @settings{qw(description options option_heading examples)};
313 root 1.8 my %url = (%config, %settings);
314     delete @url{qw(name bind stdio)};
315    
316     Getopt::Long::GetOptions (
317 root 1.13 AnyEvent::MCP::Config::http_getopt (\my $name, \my $bind, \my $print_url, \my @tokens),
318     "stdio" => \my $stdio,
319     "help|h" => \my $help,
320 root 1.8 @_,
321     ) or die $usage;
322    
323     if ($help) {
324 root 1.13 print _mcp_help $usage, @help;
325 root 1.8 exit;
326     }
327    
328     $name = AnyEvent::MCP::Config::server_name ($name // $settings{name} // $config{name});
329     my %server = (
330     %settings,
331     name => $name,
332     (defined $bind ? (bind => $bind) : ()),
333     (@tokens ? (tokens => \@tokens) : ()),
334     ($stdio ? (stdio => 1) : ()),
335     );
336    
337     if ($print_url) {
338     my %options = (%url, (@tokens ? (tokens => \@tokens) : ()));
339     my $global = AnyEvent::MCP::Config->global;
340 root 1.10 $options{path} //= $global->{section}{"instance $name"}{path} // "/";
341 root 1.8 if (($options{auth} || "") eq "none") {
342     !exists $options{tokens} or die "Cannot combine auth => none with tokens\n";
343     }
344     my $selected_bind = defined $bind ? $bind : ($settings{bind} // $config{bind});
345     print $global->http_url ($name, $selected_bind, %options), "\n";
346     exit;
347     }
348    
349     %server
350     }
351    
352 root 1.1 sub mcp_start {
353 root 1.3 require AnyEvent::MCP::Config;
354 root 1.1
355     my %kv = (%config, @_);
356 root 1.3 my $name = AnyEvent::MCP::Config::server_name ($kv{name});
357 root 1.8 my $stdio = delete $kv{stdio};
358 root 1.1
359     $kv{name} = $name;
360     $kv{server_info} ||= { name => $name, version => "0" };
361    
362     return mcp_stdio (%kv)
363 root 1.8 if $stdio || $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio";
364 root 1.1
365     my $global = AnyEvent::MCP::Config->global;
366    
367 root 1.8 my %http = $global->http_settings ($name, delete $kv{bind});
368     delete $http{tokens} if ($kv{auth} || "") eq "none";
369    
370 root 1.1 %kv = (
371     path => "/",
372 root 1.8 %http,
373 root 1.1 %kv,
374     );
375    
376 root 1.4 mcp_http (%kv)
377 root 1.1 }
378    
379     sub mcp_stdio {
380     require AnyEvent::MCP::Server::JSONL;
381    
382 root 1.11 my %kv = (%config, @_);
383     my $idle_exit = delete $kv{idle_exit};
384 root 1.1 binmode STDIN;
385     binmode STDOUT;
386     my $selected = select STDOUT;
387     $| = 1;
388     select $selected;
389    
390 root 1.11 my $server = AnyEvent::MCP::Server::JSONL->new (toolsets => [$toolset], %kv);
391 root 1.1 $server->accept (\*STDIN, \*STDOUT);
392 root 1.11
393     if ($idle_exit) {
394     $idle_exit =~ /\A[0-9]+(?:\.[0-9]+)?\z/ && $idle_exit > 0
395     or die "invalid idle_exit seconds\n";
396     if ($ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio") {
397     my ($timer, $check);
398     $check = sub {
399     return if $server->{idle_exit_requested};
400     if (grep $_->{mcp_active}, values %{$server->{connections} || {}}) {
401     $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
402     } else {
403     $server->request_idle_exit;
404     }
405     };
406     my $schedule = sub {
407     $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
408     };
409     my $on_request = $server->{on_request};
410     $server->{on_request} = sub {
411     my ($server, $req) = @_;
412 root 1.13
413 root 1.11 if ($req->{method} ne "anyevent-mcp/shutdown") {
414     $server->{idle_exit_requested} = 0;
415     $schedule->();
416     my $reply = $req->{on_reply};
417     $req->{on_reply} = sub {
418     $reply->(@_);
419     $schedule->() unless $server->{_idle_shutting_down};
420     };
421     }
422     $on_request->(@_) if $on_request;
423     };
424     my $on_close = $server->{on_close};
425     $server->{on_close} = sub {
426     undef $timer;
427     $on_close->(@_) if $on_close;
428     };
429     $schedule->();
430     }
431     }
432 root 1.1 _run $server
433     }
434    
435     sub mcp_http {
436     require AnyEvent::MCP::Server::HTTP;
437    
438     my $server = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], %config, @_);
439     $server->listen;
440     _run $server
441     }
442    
443     sub _run {
444     my ($server) = @_;
445    
446     my $cv = AnyEvent->condvar;
447     my $stopped;
448     my $stop = sub { $cv->send unless $stopped++ };
449     my $on_close = $server->{on_close};
450     $server->{on_close} = sub {
451     $on_close->(@_) if $on_close;
452     $stop->();
453     };
454    
455     if ($server->isa ("AnyEvent::MCP::Server::JSONL")) {
456     my $on_connection_close = $server->{on_connection_close};
457     $server->{on_connection_close} = sub {
458     $on_connection_close->(@_) if $on_connection_close;
459     $stop->();
460     };
461     }
462    
463     my @signals = map {
464     AnyEvent->signal (signal => $_, cb => sub { $server->close })
465     } qw(INT TERM);
466    
467     my $ok = eval {
468     $server->event (start =>);
469     $cv->recv;
470     1
471     };
472     my $error = $@;
473     $server->close;
474     die $error unless $ok;
475     return
476     }
477    
478     1