NAME
AnyEvent::MCP::Server::Toolset - MCP tool declarations and callbacks
DESCRIPTION
A Toolset answers two questions: which tools exist, and what code should run when one is called? It holds tool declarations and their callbacks, but no transport or client state. You can therefore give the same Toolset to several servers without copying or re-registering the tools.
If your program only needs one standalone server, the function interface in AnyEvent::MCP::Server::Simple manages a default Toolset for you.
EXAMPLE
use AnyEvent::MCP::Server::Toolset;
use AnyEvent::MCP::Server::HTTP;
my $tools = AnyEvent::MCP::Server::Toolset->new;
$tools->tool (echo => "Echo the supplied text",
{
type => "object", required => ["text"],
properties => { text => { type => "string" } },
},
sub {
my ($req, $args) = @_;
$req->result ({ text => $args->{text} });
},
);
my $server = AnyEvent::MCP::Server::HTTP->new (
toolsets => [$tools], tokens => [$secret],
);
Here $secret is an application-provided HTTP token. The server still needs $server->listen and an event loop; see AnyEvent::MCP::Server::HTTP. The Toolset does not create connections. For a complete standalone script, use "TUTORIAL" in AnyEvent::MCP::Server::Simple instead.
REQUEST AND TOOL WORKFLOW
Register tools with tool, then pass the Toolset to a server through its toolsets option. The server handles the protocol and transport; the Toolset supplies declarations for tools/list and callbacks for tools/call.
For a tool call, the server creates an AnyEvent::MCP::Server::Request, sets $req->{tool} and $req->{args}, and calls its on_request hook. It then finds the first Toolset containing that tool and invokes its callback as $callback-($req, $args)>. $args is the same hash reference as $req->{args}; $req->{toolset} identifies the invoking Toolset.
The input schema describes the arguments to the client; the server does not validate arguments against it. The handler must validate the arguments it uses and complete the request with one reply.
Permission checks and retries
Configure the server's permissions option with an AnyEvent::MCP::Server::Permissions object. The handler can check several paths or operations in sequence:
sub {
my ($req, $args) = @_;
my $source_grant = $req->require_permission ({
tool => $req->{tool}, subject => $args->{source},
subjects => [{ label => "source file", subject => $args->{source} }],
action => "read", sandbox_grants => ["workdir-read"],
reason => "read the source file",
});
my $destination_grant = $req->require_permission ({
tool => $req->{tool}, subject => $args->{destination},
subjects => [{ label => "destination file", subject => $args->{destination} }],
action => "write", sandbox_grants => ["workdir-write"],
reason => "write the destination file",
});
... # read using $source_grant, write using $destination_grant
$req->result ({ ... });
}
This example assumes both paths are inside the selected workdir and that the application enforces the returned grants. action chooses the kind of operation to authorize; sandbox_grants chooses the worker's authority. Grant names and their implications belong to the application. require_permission returns the granted sandbox-grant array reference, which may be smaller than the requested set. Execute using those grants rather than assuming that everything requested was allowed.
Grants are separate; Server does not combine them. When reading the source and writing the destination separately, use each grant for its corresponding operation.
Combining authority is needed when one operation spans several subjects, such as renaming a file. Request the required action and sandbox grants for every affected path: a rename needs write authority for both source and destination, unlike the read/write example above. Check that every grant still permits the operation after any reductions. For an application using workdir-write for a rename within one workdir, after requesting that grant for both paths:
my $permissions = $req->{server}{permissions};
my $source = $permissions->closure ($source_grant);
my $destination = $permissions->closure ($destination_grant);
$source->{"workdir-write"} && $destination->{"workdir-write"}
or die "Rename needs write permission for both paths\n";
closure includes implied sandbox grants. If the operation needs one combined grant set, use the intersection of these closures, rather than their union: authority granted for one path must not authorize the other. Do this before starting the operation.
A denial dies and becomes a tool error. The first missing permission throws a special exception: Server catches it and asks. Modern MCP asking replies with input_required; the client supplies the answer in a repeated call. Local asking, including the legacy fallback, retries the handler internally. An explicit local asking configuration also applies to modern callers.
The handler starts again from the beginning, including the server's on_request hook. Earlier one-time grants survive these retries but do not authorize another tool invocation. Current rules are rechecked, so a new denial can supersede an earlier approval.
Complete all checks before side effects or asynchronous work. Do not catch the permission-needed exception as an ordinary tool failure; it must reach the server's synchronous dispatch boundary. A deferred callback cannot use that boundary to restart the handler.
Replies and failures
- $req->reply ($result)
-
Sends the MCP result hash reference as supplied. Use this for arbitrary content, structured errors or large results. The server adds the JSON-RPC envelope.
- $req->result ($data)
-
Sends the hash reference
$dataasstructuredContentand also as JSON in one text content item. This duplicates the data for clients that consume only text; usereplywhen that representation is unsuitable. - $req->error ($message)
-
Sends a tool result with
isErrorset and$messageas one text item.
A synchronous handler exception becomes a tool error. Tool failures do not become JSON-RPC errors; malformed protocol requests and unknown tools are handled by Server.
Reply exactly once. Replying after completion is a bug and dies. A reply finishes the request, but returning from the callback does not.
Asynchronous work and cancellation
The callback may keep $req and return without replying. Reply when the operation completes. The callback runs in the event loop; use asynchronous operations rather than blocking it.
Set $req->{on_cancel} to stop an outstanding operation. Cancellation sets $req->{cancelled} and $req->{finished} and sends no reply. A late completion callback must check $req->{finished} before replying.
Server catches exceptions during synchronous dispatch, not exceptions in later application callbacks. Such callbacks must handle their own failures.
$req->progress ($progress, $total, $message) reports progress when the client supplied a progress token. Delivery is best effort and may be dropped by the transport; it does not complete the request.
METHODS
- $toolset = AnyEvent::MCP::Server::Toolset->new
-
Creates an empty tool registry.
- $toolset->tool ($name, $description, $input_schema, $callback->($req, $args))
-
Registers a tool named
$name. Underscores are converted to hyphens and duplicate names are rejected.$descriptionis stripped of surrounding whitespace and common indentation.$input_schemais a JSON Schema hash reference describing the arguments.$callbackreceives an AnyEvent::MCP::Server::Request object$reqand its$req->{args}hash reference as$args, for convenience:sub { my ($req, $args) = @_; $req->reply ({ content => [{ type => "text", text => $args->{text} }], }); }Registered tools are callbacks, not methods of the Toolset.
- $definitions = $toolset->list
-
Returns the array reference of MCP tool definitions in registration order.
$definitionscontains no callbacks. - $toolset->call ($req)
-
Invokes the tool named by
$req->{tool}with$reqand$req->{args}. Sets$req->{toolset}to this Toolset.Unknown names and exceptions from the callback are left for the server to handle.