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

File Contents

# Content
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 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 =head1 METHODS
33
34 =over
35
36 =item $server = AnyEvent::MCP::Server->new (%config)
37
38 Creates the protocol object without opening a listener or starting an
39 event loop. Options are:
40
41 =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 =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 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 a conflicting tool name is an error.
79
80 =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
105 Accepts the decoded JSON-RPC hash reference C<$message>. C<$reply> receives
106 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 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 =item $req = $server->json_rpc ($json, $reply->($json), %context)
123
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 =item $server->dispatch ($req)
129
130 Dispatches an already constructed Request. Normally called by C<rpc>.
131 Subclasses can override this method while retaining JSON-RPC handling.
132
133 =back
134
135 =head1 EVENTS
136
137 =over
138
139 =item on_request => $callback->($server, $req)
140
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 =item on_tools_list => $callback->($server, $req)
146
147 Replaces the default Toolset listing. C<$callback> completes C<$req>.
148
149 =item on_tools_call => $callback->($server, $req, $args)
150
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 =item on_error => $callback->($server, $req, $error)
155
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 =back
162
163 =cut
164
165 package AnyEvent::MCP::Server;
166
167 use common::sense;
168 use AnyEvent::MCP ();
169 use AnyEvent::MCP::Server::Request ();
170 use AnyEvent ();
171 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 server_info => { name => "AnyEvent::MCP", version => "$AnyEvent::MCP::VERSION" },
182 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 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 !grep { exists $_->{tool}{$name} } @{$self->{toolsets}}
199 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 $self->{instructions} = length $custom || length $short
213 ? (join "\n\n", grep length, $custom, $short, "Full instructions: meta-instructions tool description.")
214 : $instructions;
215
216 } elsif (length $custom) {
217 $self->{instructions} = join "\n\n", grep length, $custom, $instructions;
218 }
219
220 $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 $peer->{client_info} = $meta->{"io.modelcontextprotocol/clientInfo"}
279 if $version && $version ge "2026" && ref $meta->{"io.modelcontextprotocol/clientInfo"} eq "HASH";
280
281 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 # 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 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 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 my $req = AnyEvent::MCP::Server::Request->new (
344 server => $self,
345 peer => $peer,
346 id => $id,
347 method => $method,
348 params => $params,
349 user => $user,
350 protocol_version => $version,
351 on_reply => sub {
352 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 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 $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
429 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 delete $req->{progress_timer};
461 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
469 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 $result->{instructions} = $self->{instructions} if length $self->{instructions};
482
483 $req->reply ($result);
484
485 } elsif ($method eq "server/discover") {
486 my $result = {
487 supportedVersions => $self->{versions},
488 capabilities => $self->{capabilities},
489 };
490 $result->{instructions} = $self->{instructions} if length $self->{instructions};
491
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