ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Toolset.pm
Revision: 1.5
Committed: Wed Oct 21 17:28:16 2026 UTC (3 weeks ago) by root
Branch: MAIN
CVS Tags: rel-0_61, rel-0_65, rel-0_6, rel-0_62
Changes since 1.4: +27 -0 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     tool => $req->{tool}, subject => $args->{source},
69     subjects => [{ label => "source file", subject => $args->{source} }],
70 root 1.4 action => "read", sandbox_grants => ["workdir-read"],
71     reason => "read the source file",
72 root 1.1 });
73    
74     my $destination_grant = $req->require_permission ({
75     tool => $req->{tool}, subject => $args->{destination},
76     subjects => [{ label => "destination file", subject => $args->{destination} }],
77 root 1.4 action => "write", sandbox_grants => ["workdir-write"],
78     reason => "write the destination file",
79 root 1.1 });
80    
81     ... # read using $source_grant, write using $destination_grant
82     $req->result ({ ... });
83     }
84    
85 root 1.4 This example assumes both paths are inside the selected workdir and that
86     the application enforces the returned grants. C<action> chooses the kind
87     of operation to authorize; C<sandbox_grants> chooses the worker's authority.
88     Grant names and their implications belong to the application.
89     C<require_permission> returns the granted sandbox-grant array reference,
90     which may be smaller than the requested set. Execute using those grants
91     rather than assuming that everything requested was allowed.
92 root 1.1
93     Grants are separate; Server does not combine them. When reading the source
94     and writing the destination separately, use each grant for its corresponding
95     operation.
96    
97     Combining authority is needed when one operation spans several subjects,
98 root 1.4 such as renaming a file. Request the required action and sandbox grants for
99     every affected path: a rename needs write authority for both source and
100     destination, unlike the read/write example above. Check that every grant
101     still permits the operation after any reductions. For an application using
102     C<workdir-write> for a rename within one workdir, after requesting that grant
103     for both paths:
104 root 1.1
105     my $permissions = $req->{server}{permissions};
106     my $source = $permissions->closure ($source_grant);
107     my $destination = $permissions->closure ($destination_grant);
108    
109 root 1.4 $source->{"workdir-write"} && $destination->{"workdir-write"}
110     or die "Rename needs write permission for both paths\n";
111 root 1.1
112 root 1.4 C<closure> includes implied sandbox grants. If the operation needs one
113     combined grant set, use the intersection of these closures, rather than their
114 root 1.1 union: authority granted for one path must not authorize the other.
115     Do this before starting the operation.
116    
117     A denial dies and becomes a tool error. The first missing permission throws
118     a special exception: Server catches it and asks. Modern MCP asking replies
119     with C<input_required>; the client supplies the answer in a repeated call.
120     Local asking, including the legacy fallback, retries the handler internally.
121     An explicit local asking configuration also applies to modern callers.
122    
123     The handler starts again from the beginning, including the server's
124     C<on_request> hook. Earlier one-time grants survive these retries but do not
125     authorize another tool invocation. Current rules are rechecked, so a new
126     denial can supersede an earlier approval.
127    
128     Complete all checks before side effects or asynchronous work. Do not catch
129     the permission-needed exception as an ordinary tool failure; it must reach
130     the server's synchronous dispatch boundary. A deferred callback cannot use
131     that boundary to restart the handler.
132    
133     =head2 Replies and failures
134    
135     =over
136    
137     =item $req->reply ($result)
138    
139     Sends the MCP result hash reference as supplied. Use this for arbitrary
140     content, structured errors or large results. The server adds the JSON-RPC
141     envelope.
142    
143     =item $req->result ($data)
144    
145     Sends the hash reference C<$data> as C<structuredContent> and also as JSON in
146     one text content item. This duplicates the data for clients that consume
147     only text; use C<reply> when that representation is unsuitable.
148    
149     =item $req->error ($message)
150    
151     Sends a tool result with C<isError> set and C<$message> as one text item.
152    
153     =back
154    
155     A synchronous handler exception becomes a tool error. Tool failures do not
156     become JSON-RPC errors; malformed protocol requests and unknown tools are
157     handled by Server.
158    
159     Reply exactly once. Replying after completion is a bug and dies. A reply
160     finishes the request, but returning from the callback does not.
161    
162     =head2 Asynchronous work and cancellation
163    
164     The callback may keep C<$req> and return without replying. Reply when the
165     operation completes. The callback runs in the event loop; use asynchronous
166     operations rather than blocking it.
167    
168     Set C<< $req->{on_cancel} >> to stop an outstanding operation. Cancellation
169     sets C<< $req->{cancelled} >> and C<< $req->{finished} >> and sends no reply.
170     A late completion callback must check C<< $req->{finished} >> before replying.
171    
172     Server catches exceptions during synchronous dispatch, not exceptions in
173     later application callbacks. Such callbacks must handle their own failures.
174    
175     C<< $req->progress ($progress, $total, $message) >> reports progress when
176     the client supplied a progress token. Delivery is best effort and may be
177     dropped by the transport; it does not complete the request.
178    
179     =head1 METHODS
180    
181     =over
182    
183     =item $toolset = AnyEvent::MCP::Server::Toolset->new
184    
185     Creates an empty tool registry.
186    
187     =item $toolset->tool ($name, $description, $input_schema, $callback->($req, $args))
188    
189     Registers a tool named C<$name>. Underscores are converted to hyphens and
190     duplicate names are rejected.
191    
192     C<$description> is stripped of surrounding whitespace and common indentation.
193     C<$input_schema> is a JSON Schema hash reference describing the arguments.
194    
195     C<$callback> receives an L<AnyEvent::MCP::Server::Request> object C<$req>
196     and its C<< $req->{args} >> hash reference as C<$args>, for convenience:
197    
198     sub {
199     my ($req, $args) = @_;
200    
201     $req->reply ({
202     content => [{ type => "text", text => $args->{text} }],
203     });
204     }
205    
206     Registered tools are callbacks, not methods of the Toolset.
207    
208     =item $definitions = $toolset->list
209    
210     Returns the array reference of MCP tool definitions in registration order.
211     C<$definitions> contains no callbacks.
212    
213     =item $toolset->call ($req)
214    
215     Invokes the tool named by C<< $req->{tool} >> with C<$req> and
216     C<< $req->{args} >>. Sets C<< $req->{toolset} >> to this Toolset.
217    
218     Unknown names and exceptions from the callback are left for the server to
219     handle.
220    
221     =back
222    
223     =cut
224    
225     package AnyEvent::MCP::Server::Toolset;
226    
227     use common::sense;
228     use AnyEvent::MCP ();
229    
230     our @ISA = qw(AnyEvent::MCP::Base);
231    
232     sub new {
233     my $class = shift;
234    
235     $class->SUPER::new (tool => {}, list => [], @_)
236     }
237    
238     sub tool {
239     my ($self, $name, $description, $input_schema, $callback) = @_;
240    
241     $name =~ y/_/-/;
242    
243 root 1.3 !exists $self->{tool}{$name}
244     or die "duplicate MCP tool '$name'\n";
245 root 1.1
246     my $definition = {
247     name => $name,
248     description => AnyEvent::MCP::_clean_text $description,
249     inputSchema => $input_schema,
250     };
251    
252     $self->{tool}{$name} = {
253     definition => $definition,
254     callback => $callback,
255     };
256    
257     push @{$self->{list}}, $definition;
258     }
259    
260     sub list {
261     $_[0]{list}
262     }
263    
264     sub call {
265     my ($self, $req) = @_;
266 root 1.3
267 root 1.1 my $name = $req->{tool};
268    
269     my $tool = $self->{tool}{$name}
270     or die "unknown MCP tool '$name'\n";
271    
272     $req->{toolset} = $self;
273     $tool->{callback}->($req, $req->{args})
274     }
275    
276     1