| 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 |