ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server.pm
Revision: 1.16
Committed: Sun Nov 8 00:34:40 2026 UTC (2 days ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.15: +13 -0 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Server - MCP request dispatch and JSON-RPC framing
4    
5     =head1 DESCRIPTION
6    
7     An MCP server needs to understand requests, but it does not necessarily need
8     to own a listening socket. This class handles the protocol: it accepts decoded
9     JSON-RPC objects (or UTF-8 JSON), dispatches requests and delivers responses
10     through callbacks. Its subclasses L<AnyEvent::MCP::Server::JSONL> and
11     L<AnyEvent::MCP::Server::HTTP> handle the actual connections. If you are
12     writing a standalone server, L<AnyEvent::MCP::Server::Simple> is usually
13     the easier starting point.
14    
15     The server implements discovery, initialization, ping and tool listing/calls.
16     The 2025 compatibility path supports ordinary requests and responses, but
17     not server-initiated requests. Permission asking supports modern input
18     challenges as well as local helpers. SSE, subscriptions and schema validation
19     are not implemented yet.
20    
21 root 1.11 The usual object-level assembly is a
22     L<AnyEvent::MCP::Server::Toolset> holding tool callbacks, a Server
23     handling protocol messages, and a transport accepting connections.
24     Use L<AnyEvent::MCP::Server::HTTP/FIRST USE> for an executable HTTP
25     assembly or L<AnyEvent::MCP::Server::JSONL/EXAMPLE> for stdio.
26     The corresponding client entry point is L<AnyEvent::MCP::Client>.
27    
28     Only embedders that already own their transport need C<rpc> or
29     C<json_rpc> directly; these methods do not create connections or run
30     the event loop.
31    
32 root 1.1 =head1 METHODS
33    
34 root 1.5 =over
35 root 1.1
36 root 1.5 =item $server = AnyEvent::MCP::Server->new (%config)
37 root 1.1
38 root 1.5 Creates the protocol object without opening a listener or starting an
39     event loop. Options are:
40 root 1.1
41 root 1.5 =over
42    
43     =item toolsets => [$toolset...]
44    
45     L<AnyEvent::MCP::Server::Toolset> objects searched in order, defaulting to
46     an empty array. The first Toolset containing a called tool handles it.
47    
48     =item server_info => { name => $name, version => $version }
49    
50     Advertised server identity, defaulting to C<AnyEvent::MCP> and this
51     distribution's version.
52    
53     =item instructions => $text
54    
55     Optional human-readable guidance. Surrounding whitespace and common
56     indentation are stripped. Empty instructions are omitted from discovery
57     and initialization results.
58    
59 root 1.8 =item short_instructions => $text
60    
61     Optional condensed guidance for deciding when to discover this server's tools.
62     Used instead of C<instructions> in discovery when C<auto_instructions> is true.
63     Without either short or custom instructions, the full instructions remain
64     advertised.
65    
66     =item custom_instructions => $text
67    
68     Instance-specific purpose, prepended to the advertised instructions.
69    
70     =item auto_instructions => $boolean
71    
72     When true, expose full C<instructions>, prefixed by any custom instance
73     purpose, in a C<meta-instructions> tool's description and call result.
74 root 1.9 Advertise C<short_instructions> instead, when provided, preceded by any
75     custom purpose. Append a pointer to the generated tool's description when
76     short or custom instructions exist. With neither, advertise the original
77     full instructions without a redundant pointer. Defaults to false;
78 root 1.8 a conflicting tool name is an error.
79    
80 root 1.5 =item capabilities => { %capabilities }
81    
82     Advertised server capabilities, defaulting to C<{ tools =E<gt> {} }>.
83     Declaring a capability does not implement it.
84    
85     =item versions => [$version...]
86    
87     Supported revisions in preference order. Defaults to C<2026-07-28>,
88     C<2025-11-25>, C<2025-06-18> and C<2025-03-26>. The 2026 protocol uses
89     discovery and per-request metadata; the 2025 protocol uses initialization.
90    
91     =item permissions => $permissions
92    
93     L<AnyEvent::MCP::Server::Permissions> object used by
94     C<< $req->require_permission >>. The server handles permission-needed
95     exceptions. The permission object's C<ask> setting chooses between modern
96     input challenges and local prompts; local asking retries the handler
97     internally. Checks must precede side effects.
98    
99     =back
100    
101     Event callbacks are also constructor options; see L</EVENTS>.
102    
103     =item $req = $server->rpc ($message, $reply->($message), %context)
104 root 1.1
105     Accepts the decoded JSON-RPC hash reference C<$message>. C<$reply> receives
106 root 1.6 the decoded response, or undef when a notification is accepted or a request
107     is cancelled. Undef completes transport bookkeeping without a JSON-RPC
108     response. Returns the Request object for a request, or undef for
109     notifications and rejected messages.
110 root 1.1 C<$reply> can be called before this method returns, or later.
111    
112     C<peer> in C<%context> is a hash reference shared by messages on one connection.
113     It holds legacy initialization state and weak references to outstanding
114     requests for cancellation. Embedders supply a different peer for each
115     independent connection. Modern request metadata takes precedence over legacy
116     peer state.
117    
118     C<protocol_version> supplies the legacy HTTP protocol version.
119     C<on_notify> is an optional C<$callback->($req, $message)> receiving decoded
120     outgoing notifications. Without it, notifications are dropped.
121    
122 root 1.5 =item $req = $server->json_rpc ($json, $reply->($json), %context)
123 root 1.1
124     Like C<rpc>, but C<$json> and the response are UTF-8 encoded JSON strings.
125     The notification callback still receives decoded objects. Parse errors are
126     returned as JSON-RPC errors.
127    
128 root 1.5 =item $server->dispatch ($req)
129 root 1.1
130     Dispatches an already constructed Request. Normally called by C<rpc>.
131     Subclasses can override this method while retaining JSON-RPC handling.
132    
133 root 1.5 =back
134    
135 root 1.1 =head1 EVENTS
136    
137 root 1.5 =over
138    
139     =item on_request => $callback->($server, $req)
140 root 1.1
141     Called before each dispatch attempt, including permission retries. This
142     synchronous hook can inspect the request or refresh application state.
143     It does not replace dispatch; avoid side effects that must happen only once.
144    
145 root 1.5 =item on_tools_list => $callback->($server, $req)
146 root 1.1
147     Replaces the default Toolset listing. C<$callback> completes C<$req>.
148    
149 root 1.5 =item on_tools_call => $callback->($server, $req, $args)
150 root 1.1
151     Replaces the default Toolset lookup and invocation. C<$args> is also available
152     as C<< $req->{args} >>. C<$callback> completes C<$req>.
153    
154 root 1.5 =item on_error => $callback->($server, $req, $error)
155 root 1.1
156     Reports an exception caught during synchronous dispatch. A tool exception
157     becomes a text tool-error result; other exceptions become internal JSON-RPC
158     errors. Exceptions after a reply are propagated, as are exceptions from
159     asynchronous application callbacks.
160    
161 root 1.5 =back
162    
163 root 1.1 =cut
164    
165     package AnyEvent::MCP::Server;
166    
167     use common::sense;
168     use AnyEvent::MCP ();
169     use AnyEvent::MCP::Server::Request ();
170 root 1.16 use AnyEvent ();
171 root 1.1 use JSON::XS ();
172     use Scalar::Util qw(weaken);
173    
174     our @ISA = qw(AnyEvent::MCP::Base);
175    
176     sub new {
177     my $class = shift;
178    
179     my $self = $class->SUPER::new (
180     toolsets => [],
181 root 1.12 server_info => { name => "AnyEvent::MCP", version => "$AnyEvent::MCP::VERSION" },
182 root 1.1 capabilities => { tools => {} },
183     versions => [qw(2026-07-28 2025-11-25 2025-06-18 2025-03-26)],
184     @_,
185     );
186    
187     $self->{instructions} = AnyEvent::MCP::_clean_text $self->{instructions}
188     if exists $self->{instructions};
189    
190 root 1.8 my $instructions = $self->{instructions};
191     my $custom = exists $self->{custom_instructions}
192     ? AnyEvent::MCP::_clean_text $self->{custom_instructions} : "";
193    
194     if ($self->{auto_instructions} && length $instructions) {
195     require AnyEvent::MCP::Server::Toolset;
196    
197     my $name = "meta-instructions";
198 root 1.13 !grep { exists $_->{tool}{$name} } @{$self->{toolsets}}
199 root 1.8 or die "duplicate MCP tool '$name'\n";
200    
201     my $full = join "\n\n", grep length, $custom, $instructions;
202     my $toolset = AnyEvent::MCP::Server::Toolset->new;
203     $toolset->tool (meta_instructions =>
204     "Server instructions. Call for full text if truncated.\n\n$full",
205     { type => "object", properties => {} },
206     sub { $_[0]->reply ({ content => [{ type => "text", text => $full }] }) },
207     );
208     $self->{toolsets} = [@{$self->{toolsets}}, $toolset];
209    
210     my $short = exists $self->{short_instructions}
211     ? AnyEvent::MCP::_clean_text $self->{short_instructions} : "";
212 root 1.9 $self->{instructions} = length $custom || length $short
213 root 1.13 ? (join "\n\n", grep length, $custom, $short, "Full instructions: meta-instructions tool description.")
214 root 1.9 : $instructions;
215 root 1.8
216     } elsif (length $custom) {
217     $self->{instructions} = join "\n\n", grep length, $custom, $instructions;
218     }
219    
220 root 1.1 $self
221     }
222    
223     sub _rpc_error {
224     my ($self, $reply, $id, $code, $message, $data) = @_;
225    
226     my $error = { code => $code, message => $message };
227     $error->{data} = $data if defined $data;
228    
229     $reply->({ jsonrpc => "2.0", id => $id, error => $error });
230     undef
231     }
232    
233     sub json_rpc {
234     my ($self, $json, $reply, @context) = @_;
235    
236     my $send = sub { $reply->(defined $_[0] ? JSON::XS::encode_json $_[0] : undef) };
237     my $message = eval { JSON::XS::decode_json $json };
238    
239     return $self->_rpc_error ($send, undef, -32700, "Parse error")
240     if $@;
241    
242     $self->rpc ($message, $send, @context)
243     }
244    
245     sub rpc {
246     my ($self, $message, $reply, %context) = @_;
247    
248     return $self->_rpc_error ($reply, undef, -32600, "Invalid request")
249     unless ref $message eq "HASH"
250     && $message->{jsonrpc} eq "2.0"
251     && defined $message->{method} && !ref $message->{method}
252     && (!exists $message->{id} || defined $message->{id} && !ref $message->{id});
253    
254     my $id = $message->{id};
255     my $key = $id; # Hash lookups must not stringify the wire ID.
256     my $method = $message->{method};
257     my $params = exists $message->{params} ? $message->{params} : {};
258     my $peer = $context{peer} || {};
259    
260     unless (exists $message->{id}) {
261     if ($method eq "notifications/cancelled" && ref $params eq "HASH" && defined $params->{requestId}) {
262     my $req = delete $peer->{requests}{$params->{requestId}};
263     $req->cancel ($params->{reason}) if $req;
264     }
265    
266     $reply->(undef);
267     return
268     }
269    
270     return $self->_rpc_error ($reply, $id, -32602, "Invalid params")
271     unless ref $params eq "HASH"
272     && (!exists $params->{_meta} || ref $params->{_meta} eq "HASH");
273    
274     my $meta = $params->{_meta} || {};
275     my $version = $meta->{"io.modelcontextprotocol/protocolVersion"}
276     || $context{protocol_version} || $peer->{protocol_version};
277    
278 root 1.14 $peer->{client_info} = $meta->{"io.modelcontextprotocol/clientInfo"}
279     if $version && $version ge "2026" && ref $meta->{"io.modelcontextprotocol/clientInfo"} eq "HASH";
280    
281 root 1.1 if ($method eq "initialize") {
282     $version = $params->{protocolVersion};
283    
284     return $self->_rpc_error ($reply, $id, -32602, "Invalid initialize params")
285     unless defined $version && !ref $version
286     && ref $params->{capabilities} eq "HASH"
287     && ref $params->{clientInfo} eq "HASH";
288    
289     unless (grep $_ eq $version && $_ lt "2026", @{$self->{versions}}) {
290     ($version) = grep $_ lt "2026", @{$self->{versions}};
291     return $self->_rpc_error ($reply, $id, -32601, "Legacy initialization is not supported")
292     unless defined $version;
293     }
294     } else {
295     return $self->_rpc_error ($reply, $id, -32602, "Missing protocol version")
296     unless defined $version && !ref $version;
297    
298     return $self->_rpc_error ($reply, $id, -32022, "Unsupported protocol version", {
299     requested => $version,
300     supported => $self->{versions},
301     }) unless grep $_ eq $version, @{$self->{versions}};
302    
303     if ($version ge "2026") {
304     return $self->_rpc_error ($reply, $id, -32602, "Missing per-request metadata")
305     unless $meta->{"io.modelcontextprotocol/protocolVersion"} eq $version
306     && ref $meta->{"io.modelcontextprotocol/clientCapabilities"} eq "HASH";
307     }
308     }
309    
310     return $self->_rpc_error ($reply, $id, -32600, "Request id already outstanding")
311     if $peer->{requests}{$key};
312    
313 root 1.4 # the private multibroker protocol uses the normal MCP capability handshake.
314     # keep the negotiated version on this stream's peer, not on the server.
315 root 1.3 my $capabilities = $method eq "initialize"
316     ? $params->{capabilities} : $meta->{"io.modelcontextprotocol/clientCapabilities"};
317     if (ref $capabilities eq "HASH") {
318     my $extensions = $capabilities->{extensions};
319     if (ref $extensions eq "HASH") {
320     my $extension = $extensions->{"anyevent-mcp/multibroker"};
321     $peer->{aemcp_multibroker} = 1
322     if ref $extension eq "HASH" && ($extension->{version} || 0) eq "1";
323     }
324     }
325    
326 root 1.15 my $user = $context{user};
327     my $ask = $self->{permissions} && $self->{permissions}{ask};
328     my $trust = defined $ask && !ref $ask && (
329     $ask eq "mcp"
330     || $ask eq "auto" && $version ge "2026"
331     && ref $meta->{"io.modelcontextprotocol/clientCapabilities"} eq "HASH"
332     && ref $meta->{"io.modelcontextprotocol/clientCapabilities"}{elicitation} eq "HASH"
333     && (!keys %{$meta->{"io.modelcontextprotocol/clientCapabilities"}{elicitation}}
334     || ref $meta->{"io.modelcontextprotocol/clientCapabilities"}{elicitation}{form} eq "HASH")
335     );
336     if ($trust && exists $meta->{"anyevent-mcp/user"}) {
337     $user = $meta->{"anyevent-mcp/user"};
338     return $self->_rpc_error ($reply, $id, -32602, "Invalid delegated user")
339     if defined $user && (ref $user
340     || $user !~ /\A(?:global|program|instance):[A-Za-z_][A-Za-z0-9_-]*\z/);
341     }
342    
343 root 1.1 my $req = AnyEvent::MCP::Server::Request->new (
344 root 1.4 server => $self,
345     peer => $peer,
346     id => $id,
347     method => $method,
348     params => $params,
349 root 1.15 user => $user,
350 root 1.1 protocol_version => $version,
351 root 1.4 on_reply => sub {
352 root 1.1 my ($req, $result) = @_;
353    
354     delete $peer->{requests}{$key};
355    
356     if ($req->{protocol_version} ge "2026") {
357     $result = {
358     resultType => "complete",
359     %$result,
360     _meta => {
361     "io.modelcontextprotocol/serverInfo" => $self->{server_info},
362     %{$result->{_meta} || {}},
363     },
364     };
365     }
366    
367     $reply->({ jsonrpc => "2.0", id => $id, result => $result });
368     },
369     );
370    
371     if (my $notify = $context{on_notify}) {
372     $req->{on_notify} = sub {
373     my ($req, $method, $params) = @_;
374    
375     $notify->($req, { jsonrpc => "2.0", method => $method, params => $params });
376     };
377     }
378    
379 root 1.16 if ($method eq "tools/call" && $req->{on_notify} && exists $meta->{progressToken}) {
380     my $started = AE::now;
381     my $weak = $req;
382     weaken $weak;
383     $req->{progress_timer} = AnyEvent->timer (after => 30, interval => 30, cb => sub {
384     return unless $weak && !$weak->{finished};
385     my $elapsed = int (AE::now - $started);
386     $weak->progress ($elapsed, undef, "Working for ${elapsed}s");
387     });
388     }
389    
390 root 1.1 $peer->{requests}{$key} = $req;
391     weaken $peer->{requests}{$key};
392    
393     if ($method eq "tools/call") {
394     unless (defined $params->{name} && !ref $params->{name}
395     && (!exists $params->{arguments} || ref $params->{arguments} eq "HASH")) {
396     $self->_reject ($req, $reply, -32602, "Invalid tool call");
397     return $req
398     }
399    
400     $req->{tool} = $params->{name};
401     $req->{args} = exists $params->{arguments} ? $params->{arguments} : {};
402     }
403    
404     $req->{_reply} = $reply;
405    
406     $self->_dispatch ($req);
407    
408     $req
409     }
410    
411     sub _dispatch {
412     my ($self, $req) = @_;
413    
414     eval {
415     $self->{permissions}->_resume ($req)
416     if $self->{permissions} && $req->{method} eq "tools/call";
417     $self->event (request => $req);
418     $self->dispatch ($req) unless $req->{finished};
419     1
420     } or do {
421     my $error = $@;
422     die $error if $req->{finished};
423    
424     if (ref $error eq "AnyEvent::MCP::Server::Permissions::Required" && $self->{permissions}) {
425     eval {
426     $self->{permissions}->require ($req, $error->{request}, sub {
427     my ($success, $grant) = @_;
428 root 1.4
429 root 1.1 return if $req->{finished};
430     return $req->error ($grant) unless $success;
431    
432     $self->{permissions}->_remember ($req, $error->{request}, $grant);
433     $self->_dispatch ($req);
434     });
435     1
436     } or do {
437     my $failure = $@;
438     die $failure if $req->{finished};
439     $self->event (error => $req, $failure);
440     $req->error ($failure);
441     };
442     } else {
443     $self->event (error => $req, $error);
444    
445     if ($req->{method} eq "tools/call") {
446     $req->error ($error);
447     } else {
448     $self->_reject ($req, $req->{_reply}, -32603, "Internal error");
449     }
450     }
451     };
452     }
453    
454     sub _reject {
455     my ($self, $req, $reply, $code, $message) = @_;
456    
457     $req->{finished} = 1;
458     my $key = $req->{id};
459     delete $req->{peer}{requests}{$key};
460 root 1.16 delete $req->{progress_timer};
461 root 1.1 delete @$req{qw(on_reply on_notify on_cancel _reply)};
462    
463     $self->_rpc_error ($reply, $req->{id}, $code, $message)
464     }
465    
466     sub dispatch {
467     my ($self, $req) = @_;
468 root 1.4
469 root 1.1 my $method = $req->{method};
470    
471     if ($method eq "initialize") {
472     $req->{peer}{protocol_version} = $req->{protocol_version};
473     $req->{peer}{client_info} = $req->{params}{clientInfo};
474     $req->{peer}{client_capabilities} = $req->{params}{capabilities};
475    
476     my $result = {
477     protocolVersion => $req->{protocol_version},
478     serverInfo => $self->{server_info},
479     capabilities => $self->{capabilities},
480     };
481 root 1.2 $result->{instructions} = $self->{instructions} if length $self->{instructions};
482 root 1.1
483     $req->reply ($result);
484    
485     } elsif ($method eq "server/discover") {
486     my $result = {
487     supportedVersions => $self->{versions},
488     capabilities => $self->{capabilities},
489     };
490 root 1.2 $result->{instructions} = $self->{instructions} if length $self->{instructions};
491 root 1.1
492     $req->reply ($result);
493    
494     } elsif ($method eq "ping") {
495     $req->reply ({});
496    
497     } elsif ($method eq "tools/list") {
498     if ($self->{on_tools_list}) {
499     $self->event (tools_list => $req);
500     } else {
501     $req->reply ({ tools => [map @{$_->list}, @{$self->{toolsets}}] });
502     }
503    
504     } elsif ($method eq "tools/call") {
505     if ($self->{on_tools_call}) {
506     $self->event (tools_call => $req, $req->{args});
507     } else {
508     my ($toolset) = grep exists $_->{tool}{$req->{tool}}, @{$self->{toolsets}};
509    
510     if ($toolset) {
511     $toolset->call ($req);
512     } else {
513     $self->_reject ($req, $req->{_reply}, -32602, "Unknown tool '$req->{tool}'");
514     }
515     }
516    
517     } else {
518     $self->_reject ($req, $req->{_reply}, -32601, "Method not found");
519     }
520     }
521    
522     1