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