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

File Contents

# Content
1 =head1 NAME
2
3 AnyEvent::MCP::Server::Toolset - MCP tool declarations and callbacks
4
5 =head1 DESCRIPTION
6
7 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
15 =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 =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},
69 dimensions => { subject => {
70 label => "Source file", value => $args->{source},
71 choices => [{ label => "source file", match => $args->{source} }],
72 } },
73 action => "read", sandbox_grants => ["workdir-read"],
74 reason => "read the source file",
75 });
76
77 my $destination_grant = $req->require_permission ({
78 tool => $req->{tool},
79 dimensions => { subject => {
80 label => "Destination file", value => $args->{destination},
81 choices => [{ label => "destination file", match => $args->{destination} }],
82 } },
83 action => "write", sandbox_grants => ["workdir-write"],
84 reason => "write the destination file",
85 });
86
87 ... # read using $source_grant, write using $destination_grant
88 $req->result ({ ... });
89 }
90
91 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
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 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
111 my $permissions = $req->{server}{permissions};
112 my $source = $permissions->closure ($source_grant);
113 my $destination = $permissions->closure ($destination_grant);
114
115 $source->{"workdir-write"} && $destination->{"workdir-write"}
116 or die "Rename needs write permission for both paths\n";
117
118 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 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 !exists $self->{tool}{$name}
250 or die "duplicate MCP tool '$name'\n";
251
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
273 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