ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Server.pm
Revision: 1.11
Committed: Wed Oct 21 17:28:16 2026 UTC (8 days, 5 hours ago) by root
Branch: MAIN
CVS Tags: rel-0_61, rel-0_6
Changes since 1.10: +11 -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     use JSON::XS ();
171     use Scalar::Util qw(weaken);
172    
173     our @ISA = qw(AnyEvent::MCP::Base);
174    
175     sub new {
176     my $class = shift;
177    
178     my $self = $class->SUPER::new (
179     toolsets => [],
180     server_info => { name => "AnyEvent::MCP", version => $AnyEvent::MCP::VERSION },
181     capabilities => { tools => {} },
182     versions => [qw(2026-07-28 2025-11-25 2025-06-18 2025-03-26)],
183     @_,
184     );
185    
186     $self->{instructions} = AnyEvent::MCP::_clean_text $self->{instructions}
187     if exists $self->{instructions};
188    
189 root 1.8 my $instructions = $self->{instructions};
190     my $custom = exists $self->{custom_instructions}
191     ? AnyEvent::MCP::_clean_text $self->{custom_instructions} : "";
192    
193     if ($self->{auto_instructions} && length $instructions) {
194     require AnyEvent::MCP::Server::Toolset;
195    
196     my $name = "meta-instructions";
197     !grep (exists $_->{tool}{$name}, @{$self->{toolsets}})
198     or die "duplicate MCP tool '$name'\n";
199    
200     my $full = join "\n\n", grep length, $custom, $instructions;
201     my $toolset = AnyEvent::MCP::Server::Toolset->new;
202     $toolset->tool (meta_instructions =>
203     "Server instructions. Call for full text if truncated.\n\n$full",
204     { type => "object", properties => {} },
205     sub { $_[0]->reply ({ content => [{ type => "text", text => $full }] }) },
206     );
207     $self->{toolsets} = [@{$self->{toolsets}}, $toolset];
208    
209     my $short = exists $self->{short_instructions}
210     ? AnyEvent::MCP::_clean_text $self->{short_instructions} : "";
211 root 1.9 $self->{instructions} = length $custom || length $short
212     ? join ("\n\n", grep length, $custom, $short, "Full instructions: meta-instructions tool description.")
213     : $instructions;
214 root 1.8
215     } elsif (length $custom) {
216     $self->{instructions} = join "\n\n", grep length, $custom, $instructions;
217     }
218    
219 root 1.1 $self
220     }
221    
222     sub _rpc_error {
223     my ($self, $reply, $id, $code, $message, $data) = @_;
224    
225     my $error = { code => $code, message => $message };
226     $error->{data} = $data if defined $data;
227    
228     $reply->({ jsonrpc => "2.0", id => $id, error => $error });
229     undef
230     }
231    
232     sub json_rpc {
233     my ($self, $json, $reply, @context) = @_;
234    
235     my $send = sub { $reply->(defined $_[0] ? JSON::XS::encode_json $_[0] : undef) };
236     my $message = eval { JSON::XS::decode_json $json };
237    
238     return $self->_rpc_error ($send, undef, -32700, "Parse error")
239     if $@;
240    
241     $self->rpc ($message, $send, @context)
242     }
243    
244     sub rpc {
245     my ($self, $message, $reply, %context) = @_;
246    
247     return $self->_rpc_error ($reply, undef, -32600, "Invalid request")
248     unless ref $message eq "HASH"
249     && $message->{jsonrpc} eq "2.0"
250     && defined $message->{method} && !ref $message->{method}
251     && (!exists $message->{id} || defined $message->{id} && !ref $message->{id});
252    
253     my $id = $message->{id};
254     my $key = $id; # Hash lookups must not stringify the wire ID.
255     my $method = $message->{method};
256     my $params = exists $message->{params} ? $message->{params} : {};
257     my $peer = $context{peer} || {};
258    
259     unless (exists $message->{id}) {
260     if ($method eq "notifications/cancelled" && ref $params eq "HASH" && defined $params->{requestId}) {
261     my $req = delete $peer->{requests}{$params->{requestId}};
262     $req->cancel ($params->{reason}) if $req;
263     }
264    
265     $reply->(undef);
266     return
267     }
268    
269     return $self->_rpc_error ($reply, $id, -32602, "Invalid params")
270     unless ref $params eq "HASH"
271     && (!exists $params->{_meta} || ref $params->{_meta} eq "HASH");
272    
273     my $meta = $params->{_meta} || {};
274     my $version = $meta->{"io.modelcontextprotocol/protocolVersion"}
275     || $context{protocol_version} || $peer->{protocol_version};
276    
277     if ($method eq "initialize") {
278     $version = $params->{protocolVersion};
279    
280     return $self->_rpc_error ($reply, $id, -32602, "Invalid initialize params")
281     unless defined $version && !ref $version
282     && ref $params->{capabilities} eq "HASH"
283     && ref $params->{clientInfo} eq "HASH";
284    
285     unless (grep $_ eq $version && $_ lt "2026", @{$self->{versions}}) {
286     ($version) = grep $_ lt "2026", @{$self->{versions}};
287     return $self->_rpc_error ($reply, $id, -32601, "Legacy initialization is not supported")
288     unless defined $version;
289     }
290     } else {
291     return $self->_rpc_error ($reply, $id, -32602, "Missing protocol version")
292     unless defined $version && !ref $version;
293    
294     return $self->_rpc_error ($reply, $id, -32022, "Unsupported protocol version", {
295     requested => $version,
296     supported => $self->{versions},
297     }) unless grep $_ eq $version, @{$self->{versions}};
298    
299     if ($version ge "2026") {
300     return $self->_rpc_error ($reply, $id, -32602, "Missing per-request metadata")
301     unless $meta->{"io.modelcontextprotocol/protocolVersion"} eq $version
302     && ref $meta->{"io.modelcontextprotocol/clientCapabilities"} eq "HASH";
303     }
304     }
305    
306     return $self->_rpc_error ($reply, $id, -32600, "Request id already outstanding")
307     if $peer->{requests}{$key};
308    
309 root 1.4 # the private multibroker protocol uses the normal MCP capability handshake.
310     # keep the negotiated version on this stream's peer, not on the server.
311 root 1.3 my $capabilities = $method eq "initialize"
312     ? $params->{capabilities} : $meta->{"io.modelcontextprotocol/clientCapabilities"};
313     if (ref $capabilities eq "HASH") {
314     my $extensions = $capabilities->{extensions};
315     if (ref $extensions eq "HASH") {
316     my $extension = $extensions->{"anyevent-mcp/multibroker"};
317     $peer->{aemcp_multibroker} = 1
318     if ref $extension eq "HASH" && ($extension->{version} || 0) eq "1";
319     }
320     }
321    
322 root 1.1 my $req = AnyEvent::MCP::Server::Request->new (
323 root 1.4 server => $self,
324     peer => $peer,
325     id => $id,
326     method => $method,
327     params => $params,
328 root 1.1 protocol_version => $version,
329 root 1.4 on_reply => sub {
330 root 1.1 my ($req, $result) = @_;
331    
332     delete $peer->{requests}{$key};
333    
334     if ($req->{protocol_version} ge "2026") {
335     $result = {
336     resultType => "complete",
337     %$result,
338     _meta => {
339     "io.modelcontextprotocol/serverInfo" => $self->{server_info},
340     %{$result->{_meta} || {}},
341     },
342     };
343     }
344    
345     $reply->({ jsonrpc => "2.0", id => $id, result => $result });
346     },
347     );
348    
349     if (my $notify = $context{on_notify}) {
350     $req->{on_notify} = sub {
351     my ($req, $method, $params) = @_;
352    
353     $notify->($req, { jsonrpc => "2.0", method => $method, params => $params });
354     };
355     }
356    
357     $peer->{requests}{$key} = $req;
358     weaken $peer->{requests}{$key};
359    
360     if ($method eq "tools/call") {
361     unless (defined $params->{name} && !ref $params->{name}
362     && (!exists $params->{arguments} || ref $params->{arguments} eq "HASH")) {
363     $self->_reject ($req, $reply, -32602, "Invalid tool call");
364     return $req
365     }
366    
367     $req->{tool} = $params->{name};
368     $req->{args} = exists $params->{arguments} ? $params->{arguments} : {};
369     }
370    
371     $req->{_reply} = $reply;
372    
373     $self->_dispatch ($req);
374    
375     $req
376     }
377    
378     sub _dispatch {
379     my ($self, $req) = @_;
380    
381     eval {
382     $self->{permissions}->_resume ($req)
383     if $self->{permissions} && $req->{method} eq "tools/call";
384     $self->event (request => $req);
385     $self->dispatch ($req) unless $req->{finished};
386     1
387     } or do {
388     my $error = $@;
389     die $error if $req->{finished};
390    
391     if (ref $error eq "AnyEvent::MCP::Server::Permissions::Required" && $self->{permissions}) {
392     eval {
393     $self->{permissions}->require ($req, $error->{request}, sub {
394     my ($success, $grant) = @_;
395 root 1.4
396 root 1.1 return if $req->{finished};
397     return $req->error ($grant) unless $success;
398    
399     $self->{permissions}->_remember ($req, $error->{request}, $grant);
400     $self->_dispatch ($req);
401     });
402     1
403     } or do {
404     my $failure = $@;
405     die $failure if $req->{finished};
406     $self->event (error => $req, $failure);
407     $req->error ($failure);
408     };
409     } else {
410     $self->event (error => $req, $error);
411    
412     if ($req->{method} eq "tools/call") {
413     $req->error ($error);
414     } else {
415     $self->_reject ($req, $req->{_reply}, -32603, "Internal error");
416     }
417     }
418     };
419     }
420    
421     sub _reject {
422     my ($self, $req, $reply, $code, $message) = @_;
423    
424     $req->{finished} = 1;
425     my $key = $req->{id};
426     delete $req->{peer}{requests}{$key};
427     delete @$req{qw(on_reply on_notify on_cancel _reply)};
428    
429     $self->_rpc_error ($reply, $req->{id}, $code, $message)
430     }
431    
432     sub dispatch {
433     my ($self, $req) = @_;
434 root 1.4
435 root 1.1 my $method = $req->{method};
436    
437     if ($method eq "initialize") {
438     $req->{peer}{protocol_version} = $req->{protocol_version};
439     $req->{peer}{client_info} = $req->{params}{clientInfo};
440     $req->{peer}{client_capabilities} = $req->{params}{capabilities};
441    
442     my $result = {
443     protocolVersion => $req->{protocol_version},
444     serverInfo => $self->{server_info},
445     capabilities => $self->{capabilities},
446     };
447 root 1.2 $result->{instructions} = $self->{instructions} if length $self->{instructions};
448 root 1.1
449     $req->reply ($result);
450    
451     } elsif ($method eq "server/discover") {
452     my $result = {
453     supportedVersions => $self->{versions},
454     capabilities => $self->{capabilities},
455     };
456 root 1.2 $result->{instructions} = $self->{instructions} if length $self->{instructions};
457 root 1.1
458     $req->reply ($result);
459    
460     } elsif ($method eq "ping") {
461     $req->reply ({});
462    
463     } elsif ($method eq "tools/list") {
464     if ($self->{on_tools_list}) {
465     $self->event (tools_list => $req);
466     } else {
467     $req->reply ({ tools => [map @{$_->list}, @{$self->{toolsets}}] });
468     }
469    
470     } elsif ($method eq "tools/call") {
471     if ($self->{on_tools_call}) {
472     $self->event (tools_call => $req, $req->{args});
473     } else {
474     my ($toolset) = grep exists $_->{tool}{$req->{tool}}, @{$self->{toolsets}};
475    
476     if ($toolset) {
477     $toolset->call ($req);
478     } else {
479     $self->_reject ($req, $req->{_reply}, -32602, "Unknown tool '$req->{tool}'");
480     }
481     }
482    
483     } else {
484     $self->_reject ($req, $req->{_reply}, -32601, "Method not found");
485     }
486     }
487    
488     1