ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Toolset.pm
Revision: 1.6
Committed: Wed Oct 28 10:39:53 2026 UTC (7 days, 4 hours ago) by root
Branch: MAIN
CVS Tags: rel-0_7, rel-0_71, HEAD
Changes since 1.5: +10 -4 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Server::Toolset - MCP tool declarations and callbacks
4    
5     =head1 DESCRIPTION
6    
7 root 1.2 A Toolset answers two questions: which tools exist, and what code should run
8     when one is called? It holds tool declarations and their callbacks, but no
9     transport or client state. You can therefore give the same Toolset to
10     several servers without copying or re-registering the tools.
11    
12     If your program only needs one standalone server, the function interface in
13     L<AnyEvent::MCP::Server::Simple> manages a default Toolset for you.
14 root 1.1
15 root 1.5 =head1 EXAMPLE
16    
17     use AnyEvent::MCP::Server::Toolset;
18     use AnyEvent::MCP::Server::HTTP;
19    
20     my $tools = AnyEvent::MCP::Server::Toolset->new;
21     $tools->tool (echo => "Echo the supplied text",
22     {
23     type => "object", required => ["text"],
24     properties => { text => { type => "string" } },
25     },
26     sub {
27     my ($req, $args) = @_;
28     $req->result ({ text => $args->{text} });
29     },
30     );
31    
32     my $server = AnyEvent::MCP::Server::HTTP->new (
33     toolsets => [$tools], tokens => [$secret],
34     );
35    
36     Here C<$secret> is an application-provided HTTP token. The server still
37     needs C<< $server->listen >> and an event loop; see
38     L<AnyEvent::MCP::Server::HTTP>. The Toolset does not create connections.
39     For a complete standalone script, use
40     L<AnyEvent::MCP::Server::Simple/TUTORIAL> instead.
41    
42 root 1.1 =head1 REQUEST AND TOOL WORKFLOW
43    
44     Register tools with C<tool>, then pass the Toolset to a server through its
45     C<toolsets> option. The server handles the protocol and transport; the Toolset
46     supplies declarations for C<tools/list> and callbacks for C<tools/call>.
47    
48     For a tool call, the server creates an L<AnyEvent::MCP::Server::Request>,
49     sets C<< $req->{tool} >> and C<< $req->{args} >>, and calls its C<on_request>
50     hook. It then finds the first Toolset containing that tool and invokes its
51     callback as C<$callback->($req, $args)>. C<$args> is the same hash reference
52     as C<< $req->{args} >>; C<< $req->{toolset} >> identifies the invoking Toolset.
53    
54     The input schema describes the arguments to the client; the server does not
55     validate arguments against it. The handler must validate the arguments it
56     uses and complete the request with one reply.
57    
58     =head2 Permission checks and retries
59    
60     Configure the server's C<permissions> option with an
61     L<AnyEvent::MCP::Server::Permissions> object. The handler can check several
62     paths or operations in sequence:
63    
64     sub {
65     my ($req, $args) = @_;
66    
67     my $source_grant = $req->require_permission ({
68 root 1.6 tool => $req->{tool},
69     dimensions => { subject => {
70     label => "Source file", value => $args->{source},
71     choices => [{ label => "source file", match => $args->{source} }],
72     } },
73 root 1.4 action => "read", sandbox_grants => ["workdir-read"],
74     reason => "read the source file",
75 root 1.1 });
76    
77     my $destination_grant = $req->require_permission ({
78 root 1.6 tool => $req->{tool},
79     dimensions => { subject => {
80     label => "Destination file", value => $args->{destination},
81     choices => [{ label => "destination file", match => $args->{destination} }],
82     } },
83 root 1.4 action => "write", sandbox_grants => ["workdir-write"],
84     reason => "write the destination file",
85 root 1.1 });
86    
87     ... # read using $source_grant, write using $destination_grant
88     $req->result ({ ... });
89     }
90    
91 root 1.4 This example assumes both paths are inside the selected workdir and that
92     the application enforces the returned grants. C<action> chooses the kind
93     of operation to authorize; C<sandbox_grants> chooses the worker's authority.
94     Grant names and their implications belong to the application.
95     C<require_permission> returns the granted sandbox-grant array reference,
96     which may be smaller than the requested set. Execute using those grants
97     rather than assuming that everything requested was allowed.
98 root 1.1
99     Grants are separate; Server does not combine them. When reading the source
100     and writing the destination separately, use each grant for its corresponding
101     operation.
102    
103     Combining authority is needed when one operation spans several subjects,
104 root 1.4 such as renaming a file. Request the required action and sandbox grants for
105     every affected path: a rename needs write authority for both source and
106     destination, unlike the read/write example above. Check that every grant
107     still permits the operation after any reductions. For an application using
108     C<workdir-write> for a rename within one workdir, after requesting that grant
109     for both paths:
110 root 1.1
111     my $permissions = $req->{server}{permissions};
112     my $source = $permissions->closure ($source_grant);
113     my $destination = $permissions->closure ($destination_grant);
114    
115 root 1.4 $source->{"workdir-write"} && $destination->{"workdir-write"}
116     or die "Rename needs write permission for both paths\n";
117 root 1.1
118 root 1.4 C<closure> includes implied sandbox grants. If the operation needs one
119     combined grant set, use the intersection of these closures, rather than their
120 root 1.1 union: authority granted for one path must not authorize the other.
121     Do this before starting the operation.
122    
123     A denial dies and becomes a tool error. The first missing permission throws
124     a special exception: Server catches it and asks. Modern MCP asking replies
125     with C<input_required>; the client supplies the answer in a repeated call.
126     Local asking, including the legacy fallback, retries the handler internally.
127     An explicit local asking configuration also applies to modern callers.
128    
129     The handler starts again from the beginning, including the server's
130     C<on_request> hook. Earlier one-time grants survive these retries but do not
131     authorize another tool invocation. Current rules are rechecked, so a new
132     denial can supersede an earlier approval.
133    
134     Complete all checks before side effects or asynchronous work. Do not catch
135     the permission-needed exception as an ordinary tool failure; it must reach
136     the server's synchronous dispatch boundary. A deferred callback cannot use
137     that boundary to restart the handler.
138    
139     =head2 Replies and failures
140    
141     =over
142    
143     =item $req->reply ($result)
144    
145     Sends the MCP result hash reference as supplied. Use this for arbitrary
146     content, structured errors or large results. The server adds the JSON-RPC
147     envelope.
148    
149     =item $req->result ($data)
150    
151     Sends the hash reference C<$data> as C<structuredContent> and also as JSON in
152     one text content item. This duplicates the data for clients that consume
153     only text; use C<reply> when that representation is unsuitable.
154    
155     =item $req->error ($message)
156    
157     Sends a tool result with C<isError> set and C<$message> as one text item.
158    
159     =back
160    
161     A synchronous handler exception becomes a tool error. Tool failures do not
162     become JSON-RPC errors; malformed protocol requests and unknown tools are
163     handled by Server.
164    
165     Reply exactly once. Replying after completion is a bug and dies. A reply
166     finishes the request, but returning from the callback does not.
167    
168     =head2 Asynchronous work and cancellation
169    
170     The callback may keep C<$req> and return without replying. Reply when the
171     operation completes. The callback runs in the event loop; use asynchronous
172     operations rather than blocking it.
173    
174     Set C<< $req->{on_cancel} >> to stop an outstanding operation. Cancellation
175     sets C<< $req->{cancelled} >> and C<< $req->{finished} >> and sends no reply.
176     A late completion callback must check C<< $req->{finished} >> before replying.
177    
178     Server catches exceptions during synchronous dispatch, not exceptions in
179     later application callbacks. Such callbacks must handle their own failures.
180    
181     C<< $req->progress ($progress, $total, $message) >> reports progress when
182     the client supplied a progress token. Delivery is best effort and may be
183     dropped by the transport; it does not complete the request.
184    
185     =head1 METHODS
186    
187     =over
188    
189     =item $toolset = AnyEvent::MCP::Server::Toolset->new
190    
191     Creates an empty tool registry.
192    
193     =item $toolset->tool ($name, $description, $input_schema, $callback->($req, $args))
194    
195     Registers a tool named C<$name>. Underscores are converted to hyphens and
196     duplicate names are rejected.
197    
198     C<$description> is stripped of surrounding whitespace and common indentation.
199     C<$input_schema> is a JSON Schema hash reference describing the arguments.
200    
201     C<$callback> receives an L<AnyEvent::MCP::Server::Request> object C<$req>
202     and its C<< $req->{args} >> hash reference as C<$args>, for convenience:
203    
204     sub {
205     my ($req, $args) = @_;
206    
207     $req->reply ({
208     content => [{ type => "text", text => $args->{text} }],
209     });
210     }
211    
212     Registered tools are callbacks, not methods of the Toolset.
213    
214     =item $definitions = $toolset->list
215    
216     Returns the array reference of MCP tool definitions in registration order.
217     C<$definitions> contains no callbacks.
218    
219     =item $toolset->call ($req)
220    
221     Invokes the tool named by C<< $req->{tool} >> with C<$req> and
222     C<< $req->{args} >>. Sets C<< $req->{toolset} >> to this Toolset.
223    
224     Unknown names and exceptions from the callback are left for the server to
225     handle.
226    
227     =back
228    
229     =cut
230    
231     package AnyEvent::MCP::Server::Toolset;
232    
233     use common::sense;
234     use AnyEvent::MCP ();
235    
236     our @ISA = qw(AnyEvent::MCP::Base);
237    
238     sub new {
239     my $class = shift;
240    
241     $class->SUPER::new (tool => {}, list => [], @_)
242     }
243    
244     sub tool {
245     my ($self, $name, $description, $input_schema, $callback) = @_;
246    
247     $name =~ y/_/-/;
248    
249 root 1.3 !exists $self->{tool}{$name}
250     or die "duplicate MCP tool '$name'\n";
251 root 1.1
252     my $definition = {
253     name => $name,
254     description => AnyEvent::MCP::_clean_text $description,
255     inputSchema => $input_schema,
256     };
257    
258     $self->{tool}{$name} = {
259     definition => $definition,
260     callback => $callback,
261     };
262    
263     push @{$self->{list}}, $definition;
264     }
265    
266     sub list {
267     $_[0]{list}
268     }
269    
270     sub call {
271     my ($self, $req) = @_;
272 root 1.3
273 root 1.1 my $name = $req->{tool};
274    
275     my $tool = $self->{tool}{$name}
276     or die "unknown MCP tool '$name'\n";
277    
278     $req->{toolset} = $self;
279     $tool->{callback}->($req, $req->{args})
280     }
281    
282     1