ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Simple.pm
Revision: 1.8
Committed: Sun Oct 11 01:46:31 2026 UTC (4 weeks, 2 days ago) by root
Branch: MAIN
Changes since 1.7: +91 -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     { usage => "usage: $0 [OPTIONS] WORKDIR\n", path => "/" },
104     "verbose!" => \$verbose,
105     );
106     @ARGV == 1 or die "workdir required\n";
107     mcp_start %options;
108    
109     The optional leading hash specifies C<usage> text and server settings
110     such as C<path>, C<auth> or C<url_scheme>. Those settings are also returned
111     to C<mcp_start>. Without C<usage>, a basic help line is generated.
112     Standard C<--help> prints usage and exits; a parsing error dies with that
113     usage. C<--print-url> resolves the same name, bind, tokens and path as
114     startup, prints the first accepted token in the URL and exits B<without>
115     starting a listener. It requires a persistently stored master secret for
116     derived credentials and cannot predict a port requested as zero.
117    
118     All other option specifications and remaining C<@ARGV> follow the caller's
119     normal Getopt::Long configuration, including C<require_order> if selected.
120     C<--stdio> is understood by C<mcp_start> and skips HTTP configuration.
121     The helper deliberately does not enforce application-specific positional
122     arguments; C<--print-url> should not have to supply a workdir just to
123     print an address. It is a parser, not a second command language.
124    
125 root 1.1 =item mcp_start %config
126    
127     Starts and runs the usual server. When
128     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL> is C<stdio>, uses C<mcp_stdio>.
129 root 1.4 Otherwise starts HTTP without printing its URL. HTTP-capable executables
130     provide C<--print-url> to retrieve the URL explicitly.
131 root 1.1
132 root 1.3 The HTTP defaults are IPv4 loopback, a stable name-derived port in
133     16384..32767, and C</> as the endpoint path.
134 root 1.7 Without explicit C<tokens>, one server-specific token is derived from
135     the private global secret. The selected token authenticates either its URL
136     path or an
137 root 1.5 C<Authorization: Bearer TOKEN> header at the clean URL returned by
138 root 1.6 C<< $server->url (0) >>. The corresponding executable's C<--print-url>
139     option prints the first accepted token in its URL.
140 root 1.1
141     C<name> selects the C<[server NAME]> configuration section and credential.
142 root 1.3 It defaults to the program's basename.
143 root 1.5 Named settings C<bind>, C<path>, C<url_host>, C<url_scheme> and C<tokens>
144 root 1.7 are loaded from the global configuration. Explicit C<tokens> (an ordered,
145     nonempty array reference of URL-safe strings) replaces the configured list
146     and the derived default. Every token works in the URL and bearer header.
147 root 1.3 An explicit C<bind> overrides the configured bind address. Other arguments
148 root 1.7 override these settings.
149 root 1.1
150     The global secret is generated automatically when needed; see
151     L<AnyEvent::MCP/GLOBAL CONFIGURATION>. No configuration is loaded for stdio.
152 root 1.4 HTTP startup neither prints to stdout nor changes its buffering.
153 root 1.7 C<mcp_start> does not parse C<@ARGV>; applications parse their own options.
154 root 1.8 The bundled executables use C<mcp_getoptions>, which handles shared
155     options and URL discovery before starting the event loop.
156 root 1.1
157     =item mcp_stdio %config
158    
159     Creates and runs a server over stdin/stdout. Arguments override the collected
160     configuration. Stdout buffering is disabled. On stdin EOF, pending replies
161     finish and stdout drains before
162     this function returns. Stream errors close the connection and cancel pending
163     requests.
164    
165     =item mcp_http %config
166    
167     Creates and runs an HTTP server. For example:
168    
169     mcp_http
170     host => "127.0.0.1",
171     port => 31740,
172 root 1.7 path => "/mcp/",
173     tokens => ["your-long-random-secret"];
174 root 1.1
175     Arguments override the collected configuration. Unlike C<mcp_start>, this
176 root 1.8 function does not derive or supply authentication. B<Always> pass C<tokens>,
177     or explicitly pass C<auth =E<gt> "none"> when a trusted embedding layer
178     enforces access. Omitting both is an error, not an unauthenticated default.
179     It does not print a URL; use
180 root 1.7 C<on_start> for application-specific startup output. Neither function
181     implements OAuth or Basic authentication. See
182     L<AnyEvent::MCP::Server::HTTP/AUTHENTICATION> for details.
183 root 1.1
184     All three starters return when the server is closed or on C<SIGINT> or
185     C<SIGTERM>. Signal shutdown closes connections and cancels outstanding
186     requests. The signal watchers belong to the blocking call.
187    
188     =back
189    
190     =cut
191    
192     package AnyEvent::MCP::Server::Simple;
193    
194     use common::sense;
195     use AnyEvent ();
196     use AnyEvent::MCP ();
197     use AnyEvent::MCP::Server::Toolset ();
198     use Exporter ();
199    
200     our @ISA = qw(Exporter);
201 root 1.8 our @EXPORT = qw(mcp_config mcp_tool mcp_getoptions mcp_start mcp_stdio mcp_http);
202 root 1.1
203     my %config;
204     my $toolset = AnyEvent::MCP::Server::Toolset->new;
205    
206     sub _run;
207    
208     sub mcp_config {
209     my %kv = @_;
210    
211     $kv{instructions} = AnyEvent::MCP::_clean_text $kv{instructions}
212     if exists $kv{instructions};
213    
214     @config{keys %kv} = values %kv;
215     }
216    
217     sub mcp_tool {
218     $toolset->tool (@_)
219     }
220    
221 root 1.8 sub mcp_getoptions {
222     require Getopt::Long;
223     require AnyEvent::MCP::Config;
224    
225     my $settings = ref $_[0] eq "HASH" ? shift : {};
226     my %settings = %$settings;
227     my $usage = delete $settings{usage}
228     // "usage: $0 [--name NAME] [--bind ADDRESS[:PORT]] [--token TOKEN ...] [--stdio] [--print-url] [OPTIONS] [ARGS...]\n";
229     my %url = (%config, %settings);
230     delete @url{qw(name bind stdio)};
231    
232     my ($name, $bind, $stdio, $print_url, $help, @tokens);
233     Getopt::Long::GetOptions (
234     AnyEvent::MCP::Config::http_getopt (\$name, \$bind, \$print_url, \@tokens),
235     "stdio" => \$stdio,
236     "help|h" => \$help,
237     @_,
238     ) or die $usage;
239    
240     if ($help) {
241     print $usage;
242     exit;
243     }
244    
245     $name = AnyEvent::MCP::Config::server_name ($name // $settings{name} // $config{name});
246     my %server = (
247     %settings,
248     name => $name,
249     (defined $bind ? (bind => $bind) : ()),
250     (@tokens ? (tokens => \@tokens) : ()),
251     ($stdio ? (stdio => 1) : ()),
252     );
253    
254     if ($print_url) {
255     my %options = (%url, (@tokens ? (tokens => \@tokens) : ()));
256     my $global = AnyEvent::MCP::Config->global;
257     $options{path} //= $global->{section}{"server $name"}{path} // "/";
258     if (($options{auth} || "") eq "none") {
259     !exists $options{tokens} or die "Cannot combine auth => none with tokens\n";
260     }
261     my $selected_bind = defined $bind ? $bind : ($settings{bind} // $config{bind});
262     print $global->http_url ($name, $selected_bind, %options), "\n";
263     exit;
264     }
265    
266     %server
267     }
268    
269 root 1.1 sub mcp_start {
270 root 1.3 require AnyEvent::MCP::Config;
271 root 1.1
272     my %kv = (%config, @_);
273 root 1.3 my $name = AnyEvent::MCP::Config::server_name ($kv{name});
274 root 1.8 my $stdio = delete $kv{stdio};
275 root 1.1
276     $kv{name} = $name;
277     $kv{server_info} ||= { name => $name, version => "0" };
278    
279     return mcp_stdio (%kv)
280 root 1.8 if $stdio || $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio";
281 root 1.1
282     my $global = AnyEvent::MCP::Config->global;
283    
284 root 1.8 my %http = $global->http_settings ($name, delete $kv{bind});
285     delete $http{tokens} if ($kv{auth} || "") eq "none";
286    
287 root 1.1 %kv = (
288     path => "/",
289 root 1.8 %http,
290 root 1.1 %kv,
291     );
292    
293 root 1.4 mcp_http (%kv)
294 root 1.1 }
295    
296     sub mcp_stdio {
297     require AnyEvent::MCP::Server::JSONL;
298    
299     binmode STDIN;
300     binmode STDOUT;
301     my $selected = select STDOUT;
302     $| = 1;
303     select $selected;
304    
305     my $server = AnyEvent::MCP::Server::JSONL->new (toolsets => [$toolset], %config, @_);
306     $server->accept (\*STDIN, \*STDOUT);
307     _run $server
308     }
309    
310     sub mcp_http {
311     require AnyEvent::MCP::Server::HTTP;
312    
313     my $server = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], %config, @_);
314     $server->listen;
315     _run $server
316     }
317    
318     sub _run {
319     my ($server) = @_;
320    
321     my $cv = AnyEvent->condvar;
322     my $stopped;
323     my $stop = sub { $cv->send unless $stopped++ };
324     my $on_close = $server->{on_close};
325     $server->{on_close} = sub {
326     $on_close->(@_) if $on_close;
327     $stop->();
328     };
329    
330     if ($server->isa ("AnyEvent::MCP::Server::JSONL")) {
331     my $on_connection_close = $server->{on_connection_close};
332     $server->{on_connection_close} = sub {
333     $on_connection_close->(@_) if $on_connection_close;
334     $stop->();
335     };
336     }
337    
338     my @signals = map {
339     AnyEvent->signal (signal => $_, cb => sub { $server->close })
340     } qw(INT TERM);
341    
342     my $ok = eval {
343     $server->event (start =>);
344     $cv->recv;
345     1
346     };
347     my $error = $@;
348     $server->close;
349     die $error unless $ok;
350     return
351     }
352    
353     1