ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Simple.pm
Revision: 1.3
Committed: Sat Oct 10 10:48:51 2026 UTC (4 weeks, 1 day ago) by root
Branch: MAIN
Changes since 1.2: +10 -18 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     =item mcp_start %config
96    
97     Starts and runs the usual server. When
98     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL> is C<stdio>, uses C<mcp_stdio>.
99     Otherwise starts HTTP and prints its cookie URL to stdout.
100    
101 root 1.3 The HTTP defaults are IPv4 loopback, a stable name-derived port in
102     16384..32767, and C</> as the endpoint path.
103     The same derived token authenticates either the cookie
104 root 1.1 URL or an C<Authorization: Bearer TOKEN> header at the clean URL returned by
105     C<< $server->url (0) >>. C<anyevent-mcp token NAME> prints this token.
106    
107     C<name> selects the C<[server NAME]> configuration section and credential.
108 root 1.3 It defaults to the program's basename.
109     Named settings C<bind>, C<path>, C<url_host>, C<url_scheme>, C<cookie>,
110     C<bearer> and C<users> are loaded from the global configuration.
111     An explicit C<bind> overrides the configured bind address. Other arguments
112 root 1.1 override these settings. Explicit authentication options replace the default authentication.
113    
114     The global secret is generated automatically when needed; see
115     L<AnyEvent::MCP/GLOBAL CONFIGURATION>. No configuration is loaded for stdio.
116     The URL is printed before C<on_start> is called. Output buffering is left
117     to the application.
118    
119     =item mcp_stdio %config
120    
121     Creates and runs a server over stdin/stdout. Arguments override the collected
122     configuration. Stdout buffering is disabled. On stdin EOF, pending replies
123     finish and stdout drains before
124     this function returns. Stream errors close the connection and cancel pending
125     requests.
126    
127     =item mcp_http %config
128    
129     Creates and runs an HTTP server. For example:
130    
131     mcp_http
132     host => "127.0.0.1",
133     port => 31740,
134     path => "/mcp/";
135    
136     Arguments override the collected configuration. Unlike C<mcp_start>, this
137     function does not supply cookie authentication or print a URL; use
138     C<on_start> for application-specific startup output.
139    
140     All three starters return when the server is closed or on C<SIGINT> or
141     C<SIGTERM>. Signal shutdown closes connections and cancels outstanding
142     requests. The signal watchers belong to the blocking call.
143    
144     =back
145    
146     =cut
147    
148     package AnyEvent::MCP::Server::Simple;
149    
150     use common::sense;
151     use AnyEvent ();
152     use AnyEvent::MCP ();
153     use AnyEvent::MCP::Server::Toolset ();
154     use Exporter ();
155    
156     our @ISA = qw(Exporter);
157     our @EXPORT = qw(mcp_config mcp_tool mcp_start mcp_stdio mcp_http);
158    
159     my %config;
160     my $toolset = AnyEvent::MCP::Server::Toolset->new;
161    
162     sub _run;
163    
164     sub mcp_config {
165     my %kv = @_;
166    
167     $kv{instructions} = AnyEvent::MCP::_clean_text $kv{instructions}
168     if exists $kv{instructions};
169    
170     @config{keys %kv} = values %kv;
171     }
172    
173     sub mcp_tool {
174     $toolset->tool (@_)
175     }
176    
177     sub mcp_start {
178 root 1.3 require AnyEvent::MCP::Config;
179 root 1.1
180     my %kv = (%config, @_);
181 root 1.3 my $name = AnyEvent::MCP::Config::server_name ($kv{name});
182 root 1.1
183     $kv{name} = $name;
184     $kv{server_info} ||= { name => $name, version => "0" };
185    
186     return mcp_stdio (%kv)
187     if $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio";
188    
189     my $global = AnyEvent::MCP::Config->global;
190    
191     %kv = (
192     path => "/",
193 root 1.3 $global->http_settings ($name, delete $kv{bind}),
194 root 1.1 %kv,
195     );
196    
197     my $on_start = $kv{on_start};
198     mcp_http (%kv, on_start => sub {
199     my ($server) = @_;
200    
201     print $server->url, "\n";
202     $on_start->($server) if $on_start;
203     })
204     }
205    
206     sub mcp_stdio {
207     require AnyEvent::MCP::Server::JSONL;
208    
209     binmode STDIN;
210     binmode STDOUT;
211     my $selected = select STDOUT;
212     $| = 1;
213     select $selected;
214    
215     my $server = AnyEvent::MCP::Server::JSONL->new (toolsets => [$toolset], %config, @_);
216     $server->accept (\*STDIN, \*STDOUT);
217     _run $server
218     }
219    
220     sub mcp_http {
221     require AnyEvent::MCP::Server::HTTP;
222    
223     my $server = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], %config, @_);
224     $server->listen;
225     _run $server
226     }
227    
228     sub _run {
229     my ($server) = @_;
230    
231     my $cv = AnyEvent->condvar;
232     my $stopped;
233     my $stop = sub { $cv->send unless $stopped++ };
234     my $on_close = $server->{on_close};
235     $server->{on_close} = sub {
236     $on_close->(@_) if $on_close;
237     $stop->();
238     };
239    
240     if ($server->isa ("AnyEvent::MCP::Server::JSONL")) {
241     my $on_connection_close = $server->{on_connection_close};
242     $server->{on_connection_close} = sub {
243     $on_connection_close->(@_) if $on_connection_close;
244     $stop->();
245     };
246     }
247    
248     my @signals = map {
249     AnyEvent->signal (signal => $_, cb => sub { $server->close })
250     } qw(INT TERM);
251    
252     my $ok = eval {
253     $server->event (start =>);
254     $cv->recv;
255     1
256     };
257     my $error = $@;
258     $server->close;
259     die $error unless $ok;
260     return
261     }
262    
263     1