ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Server/Simple.pm
Revision: 1.56
Committed: Sun Nov 8 00:55:41 2026 UTC (33 hours, 14 minutes ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.55: +6 -9 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 =head1 NAME
2
3 AnyEvent::MCP::Server::Simple - simple function interface for MCP servers
4
5 =head1 SYNOPSIS
6
7 use AnyEvent::MCP::Server::Simple;
8
9 mcp_config
10 server_name => "example",
11 server_version => 0,
12 instructions => <<'EOF';
13 Use echo to return the supplied text.
14 EOF
15
16 mcp_tool echo =>
17 "Return the supplied text.",
18 {
19 type => "object",
20 properties => { text => { type => "string" } },
21 required => ["text"],
22 },
23 sub {
24 my ($req, $args) = @_;
25
26 $req->reply ({
27 content => [{ type => "text", text => $args->{text} }],
28 });
29 };
30
31 mcp_start;
32
33 =head1 DESCRIPTION
34
35 For most MCP servers, registering a few tools and starting a transport is
36 enough. This module does just that: call C<mcp_tool> to register callbacks,
37 optionally configure the server with C<mcp_config>, then start it with
38 C<mcp_start>, C<mcp_stdio> or C<mcp_http>. Registration alone does not start
39 a server or an event loop.
40
41 Human-readable descriptions and instructions are stripped of surrounding
42 whitespace and common indentation when registered, so indented here-documents
43 can be used naturally.
44
45 C<mcp_start>, C<mcp_stdio> and C<mcp_http> are alternative blocking entry
46 points for a self-contained server. They run AnyEvent until shutdown; you
47 do not start another event loop. If your application already owns the server
48 lifetime or needs several transports, use the object interfaces instead.
49
50 Simple uses L<AnyEvent::MCP::Server> and a default Toolset internally; it is
51 not a second server implementation. Applications needing custom dispatch,
52 such as a multibroker, can use the Server API directly.
53
54 Start with L</TUTORIAL> for a complete server and a first client call.
55 For running the bundled servers without writing Perl, see
56 L<AnyEvent::MCP::Intro>.
57
58 =head1 TUTORIAL
59
60 This tutorial builds a small server, calls it, then adds options, permission
61 checks and asynchronous work. You do not need to implement JSON-RPC,
62 authentication or a transport. Simple supplies those parts; your program
63 declares tools and handles their arguments.
64
65 For installation, connecting AI clients and configuring the bundled servers,
66 see L<AnyEvent::MCP::Intro>. The function reference follows this tutorial.
67
68 =head2 A complete first server
69
70 Save the following as C<echo-mcp>:
71
72 #!/opt/bin/perl
73
74 use common::sense;
75 use AnyEvent::MCP::Server::Simple;
76
77 my $options = mcp_getoptions {
78 name => "echo-demo",
79 usage => "Usage: $0 [OPTION]...\n",
80 description => "Return text supplied to the echo tool.",
81 };
82
83 @ARGV == 0
84 or die "Unexpected arguments\n";
85
86 mcp_config
87 server_name => "echo-demo",
88 server_version => 1,
89 instructions => "Use echo to return the supplied text.";
90
91 mcp_tool echo =>
92 "Return the supplied text.",
93 {
94 type => "object",
95 properties => {
96 text => {
97 type => "string",
98 maxLength => 4096,
99 },
100 },
101 required => ["text"],
102 },
103 sub {
104 my ($req, $args) = @_;
105
106 my $text = $args->{text};
107 defined $text and !ref $text and length $text <= 4096
108 or die "text must be a string of at most 4096 characters\n";
109
110 $req->reply ({
111 content => [{ type => "text", text => $text }],
112 });
113 };
114
115 mcp_start %$options;
116
117 Use your installed Perl's explicit path in the shebang, or invoke the file
118 with C<perl> as in the commands below.
119
120 C<mcp_getoptions> parses the common server options and returns startup
121 settings. C<mcp_config> declares protocol metadata and instructions.
122 C<mcp_tool> registers a description, input schema and callback.
123 C<mcp_start> constructs the transport and runs AnyEvent until shutdown.
124 Register everything before starting: code after C<mcp_start> runs only
125 when the server stops.
126
127 The input schema tells the client what to supply. The server does B<not>
128 validate arguments against JSON Schema, so the callback checks the value it
129 actually uses. A missing text, array or object fails as a tool error.
130 The example allows an empty string.
131
132 C<$req> is an L<AnyEvent::MCP::Server::Request>; C<$args> is its argument
133 hash reference. Replying finishes the request. Merely returning from the
134 callback does not. You supply the MCP result, and the server supplies the
135 JSON-RPC envelope.
136
137 =head2 Start and call the server
138
139 Inspect the common options with:
140
141 perl echo-mcp --help
142
143 For a client launching a local subprocess, configure:
144
145 perl /absolute/path/to/echo-mcp --stdio
146
147 The client must launch the process itself. Stdin and stdout carry MCP,
148 so starting the program in a terminal and typing text at it is not a test
149 of the echo tool. Print diagnostics to stderr, not stdout.
150
151 You can make a first call without an AI client. Save this as C<try-echo>
152 in the same directory as C<echo-mcp>:
153
154 use common::sense;
155 use AnyEvent;
156 use AnyEvent::MCP::Client::JSONL;
157
158 my $client = AnyEvent::MCP::Client::JSONL->new (
159 command => [$^X, "./echo-mcp", "--stdio"],
160 );
161
162 my $cv = AE::cv;
163
164 $client->call (echo => { text => "hello" }, sub {
165 $cv->send (@_);
166 });
167
168 my ($success, $result) = $cv->recv;
169 $success
170 or die "$result->{message}\n";
171 !$result->{isError}
172 or die "$result->{content}[0]{text}\n";
173
174 print $result->{content}[0]{text}, "\n";
175
176 Run C<perl try-echo>. It prints:
177
178 hello
179
180 The JSONL client starts the server, connects and negotiates the protocol on
181 first use. C<$^X> chooses the same Perl interpreter as the test client.
182 The condition variable keeps the event loop running until the reply arrives.
183 Keep C<$client> alive while its operations are outstanding.
184
185 There are two success checks. C<$success> means the JSON-RPC operation
186 completed successfully; a tool can still return C<isError> in that result.
187 This client expects our echo tool's text result. A general client must handle
188 other MCP content representations too.
189
190 To use HTTP, start the server in one terminal:
191
192 perl echo-mcp --bind :31742
193
194 and retrieve its URL in another:
195
196 perl echo-mcp --bind :31742 --print-url
197
198 Configure that URL in your MCP client and ask it to call echo with text
199 C<hello>. Both commands use instance C<echo-demo>, unless you override it
200 with C<--name>. Repeat the same listener overrides when printing the URL.
201 C<--print-url> prints the address and exits; it does not start a server.
202
203 For the small Perl client above, replace the JSONL import and constructor
204 with:
205
206 use AnyEvent::MCP::Client::HTTP;
207
208 my $client = AnyEvent::MCP::Client::HTTP->new (
209 url => $ARGV[0],
210 );
211
212 Pass the printed URL as its first command-line argument.
213 The rest of the call and result handling stays the same.
214
215 C<mcp_start> supplies named HTTP settings and a token derived from the
216 private global secret. Stdio needs no HTTP configuration. C<name> selects
217 local settings and credentials; C<server_name> identifies the advertised
218 MCP implementation. The two need not be the same.
219 See L<AnyEvent::MCP::Intro/CONFIGURING YOUR SETUP>.
220
221 =head2 Add an application option
222
223 Suppose you want the server user to choose the maximum text length.
224 Replace the original C<mcp_getoptions> call with:
225
226 my $max_length = 4096;
227
228 my $options = mcp_getoptions (
229 {
230 name => "echo-demo",
231 usage => "Usage: $0 [OPTION]...\n",
232 description => "Return text supplied to the echo tool.",
233 options => [
234 ["--max-length N", "Maximum text length in characters"],
235 ],
236 },
237 "max-length=i" => \$max_length,
238 );
239
240 $max_length > 0
241 or die "--max-length must be positive\n";
242
243 Use C<$max_length> in both the schema's C<maxLength> and the callback's
244 length check. The server user can then start:
245
246 perl echo-mcp --stdio --max-length 1000
247
248 The option specification is passed to L<Getopt::Long>. The C<options>
249 entries supply human-readable help; they do not register options.
250 The later tool examples use the original 4096-character limit; use your
251 chosen limit in them too if you keep this option.
252 Common options such as C<--stdio>, C<--bind> and C<--help> remain available.
253
254 If your server also takes positional arguments, validate the remaining
255 C<@ARGV> after C<mcp_getoptions>. It handles C<--help> and C<--print-url>
256 before that validation, so the user can retrieve an HTTP URL without
257 supplying an application workdir or starting the application.
258
259 Settings affecting URL discovery, such as C<name> and C<path>, should be
260 available to C<mcp_getoptions> through its leading settings hash or an
261 earlier C<mcp_config> call. A setting supplied only after the parser exits
262 for C<--print-url> cannot affect that printed URL.
263
264 =head2 Choose a result representation
265
266 Our echo callback returns one text content item. Use C<reply> when the
267 representation matters: text, other content types, a larger result or a
268 particular MCP result shape.
269
270 For an ordinary result object, you can replace the reply with:
271
272 $req->result ({ text => $text, length => length $text });
273
274 C<result> returns that hash as C<structuredContent> and also serializes it
275 as JSON in one text content item. Clients consuming only text can still
276 see the data, but it is deliberately duplicated. Wrap arrays or scalars in
277 an object, or use C<reply> to choose a different representation.
278
279 To report a tool failure explicitly:
280
281 $req->error ("The requested operation cannot be completed");
282
283 A synchronous exception from the handler also becomes a tool-error result.
284 Malformed protocol requests and an unknown tool name are handled by the
285 server. Your callback does not construct a JSON-RPC response itself.
286
287 Reply once. Code that calls C<reply>, C<result> or C<error> and then calls
288 another reply method is a programming error. Keep a final reply separate
289 from the work leading to it so the completion point is easy to see.
290
291 =head2 Add a permission check before changing state
292
293 Authentication alone does not decide which tool operations a user allows.
294 For an example with a side effect but no external worker, add a tool that
295 stores one note in the server's memory.
296
297 Add this before registering the tools:
298
299 use AnyEvent::MCP::Server::Permissions;
300
301 my $note;
302
303 mcp_permissions +AnyEvent::MCP::Server::Permissions->new (
304 name => "echo-notes",
305 actions => {
306 remember => "Replace the server's in-memory note",
307 },
308 );
309
310 Then register this tool before C<mcp_start>:
311
312 mcp_tool remember =>
313 "Replace the server's in-memory note with the supplied text.",
314 {
315 type => "object",
316 properties => {
317 text => { type => "string", maxLength => 4096 },
318 },
319 required => ["text"],
320 },
321 sub {
322 my ($req, $args) = @_;
323
324 my $text = $args->{text};
325 defined $text and !ref $text and length $text <= 4096
326 or die "text must be a string of at most 4096 characters\n";
327
328 $req->require_permission ({
329 tool => $req->{tool},
330 action => "remember",
331 sandbox_grants => [],
332 reason => "replace the server's in-memory note",
333 });
334
335 $note = $text;
336
337 $req->result ({ remembered => length $note });
338 };
339
340 Restart the server and call remember through your client. The default
341 permission helper asks on the server machine. Allow once permits this
342 invocation. Allow always saves a rule for action C<remember>; subsequent
343 matching calls do not ask again. The note itself lasts only for this
344 server process and is shared by its clients.
345
346 The permission engine's name C<echo-notes> selects its default policy file:
347
348 ~/.config/anyevent-mcp/rules/echo-notes.ini
349
350 The actual directory follows C<XDG_CONFIG_HOME> when set.
351 This is the shared engine's default convention, different from mcp-fs's
352 C<rules/fs-NAME.ini>. Manage it using C<anyevent-mcp manage-rules-file>
353 with the actual pathname, or supply C<file> to the engine explicitly.
354
355 For example, a manually written allow rule is:
356
357 [rule]
358 decision=allow
359 action=remember
360 sandbox-grants=
361
362 The empty grant list is intentional. This tool updates an in-memory value;
363 it does not launch a sandboxed worker. C<action> is the operation class
364 to authorize, while C<sandbox_grants> describes worker authority.
365
366 The permission engine checks policy; it does not restrict filesystem or
367 network access by itself. If you add file or command tools, define the
368 necessary grants and enforce the returned set through your worker or
369 sandbox. Do not copy C<sandbox_grants =E<gt> []> into a file-writing
370 handler and assume it confines access.
371
372 Here the permission name is deliberately fixed and independent of the
373 HTTP instance name. If you want each C<--name> to have separate policy,
374 pass C<$options{name}> as the permission engine's name instead.
375
376 =head2 Why checks must precede side effects
377
378 When permission is missing, C<require_permission> throws a special
379 exception. Server catches it, asks, and may invoke the handler again.
380 The assignment to C<$note> therefore comes B<after> the check.
381
382 With a local prompt, the server retries internally. Modern MCP form asking
383 returns C<input_required>; the client repeats the call with its answer.
384 Earlier one-time grants survive permission retries within this invocation,
385 but do not authorize the next call. Saved rules are rechecked.
386
387 Do not catch that exception as an ordinary failure around the permission
388 check. It must reach Server's synchronous dispatch boundary.
389 For several checks, complete all of them before modifying state or starting
390 asynchronous work. An C<on_request> hook also runs on retry, so it must not
391 perform a once-only side effect before the handler's checks.
392
393 To choose client-side form asking for this example:
394
395 PERL_ANYEVENT_MCP_PERMISSION_ASK=auto perl echo-mcp --stdio
396
397 C<auto> uses modern form elicitation when the client advertises support,
398 otherwise the local helper. C<mcp> requires that support; C<helper> keeps
399 the prompt local. The tiny try-echo client advertises no form support,
400 so auto uses the helper there. To handle forms in a Perl client, see
401 L<AnyEvent::MCP::Client/on_request> and
402 L<AnyEvent::MCP::Permissions::Ask/elicitation>.
403
404 For grant definitions, subject scopes and multi-path operations, see
405 L<AnyEvent::MCP::Server::Permissions> and
406 L<AnyEvent::MCP::Server::Toolset/Permission checks and retries>.
407
408 =head2 Let an asynchronous operation finish later
409
410 A tool callback runs in the AnyEvent event loop. Blocking it with C<sleep>
411 or a synchronous long-running command also blocks other requests.
412
413 Add C<use AnyEvent;> and register the following before C<mcp_start>:
414
415 mcp_tool delayed_echo =>
416 "Return the supplied text after a delay of up to 60 seconds.",
417 {
418 type => "object",
419 properties => {
420 text => { type => "string", maxLength => 4096 },
421 seconds => {
422 type => "integer",
423 minimum => 0,
424 maximum => 60,
425 },
426 },
427 required => ["text", "seconds"],
428 },
429 sub {
430 my ($req, $args) = @_;
431
432 my $text = $args->{text};
433 defined $text and !ref $text and length $text <= 4096
434 or die "text must be a string of at most 4096 characters\n";
435
436 my $seconds = $args->{seconds};
437 defined $seconds and !ref $seconds
438 and $seconds =~ /\A[0-9]+\z/ and $seconds <= 60
439 or die "seconds must be an integer from 0 to 60\n";
440
441 $req->progress (0, 1, "Waiting");
442
443 my $timer = AnyEvent->timer (after => $seconds, cb => sub {
444 $req->{finished}
445 and return;
446
447 $req->progress (1, 1, "Done");
448 $req->reply ({
449 content => [{ type => "text", text => $text }],
450 });
451 });
452
453 $req->{on_cancel} = sub {
454 undef $timer;
455 };
456 };
457
458 The tool is advertised as C<delayed-echo>, because registration converts
459 underscores to hyphens. Ask the client to call it with text C<hello> and
460 seconds C<2>. The callback returns immediately; the timer supplies the reply
461 later. Other requests can run while it waits.
462
463 Keep watcher objects alive while they are needed. Here the cancellation
464 callback retains C<$timer>, and the timer's callback retains C<$req> and
465 C<$text>. Cancellation releases the timer. Replying finishes the request
466 and removes its cancellation callback, releasing the watcher as well.
467 No separate job registry is needed for this example.
468
469 Check C<finished> in late completion callbacks before replying.
470 Cancellation sets both C<finished> and C<cancelled> and sends no reply.
471 For subprocesses or network work, the cancellation callback should stop
472 that operation and release its resources instead of merely abandoning
473 the eventual reply.
474
475 Server catches exceptions during synchronous dispatch, not exceptions
476 from a later timer or I/O callback. If an asynchronous operation can fail,
477 handle that failure in its completion callback and report it with
478 C<error> or C<reply>, provided the request is still active.
479
480 C<progress> only sends when the caller supplied a progress token.
481 An outstanding tool request with a progress token emits an automatic
482 elapsed-time heartbeat every 30 seconds. Delivery is best effort:
483 stdio sends notifications directly, while Streamable HTTP sends them
484 over the request's SSE response stream. Clients may ignore progress.
485
486 =head2 When to use the object interfaces
487
488 Simple owns one default Toolset and the blocking server lifetime.
489 That is useful for a small standalone program. If you already run an
490 event loop, want multiple transports or tool registries, or need custom
491 protocol dispatch, construct the objects directly.
492
493 L<AnyEvent::MCP::Server::Toolset> registers reusable tools.
494 L<AnyEvent::MCP::Server> handles the protocol.
495 L<AnyEvent::MCP::Server::HTTP> and L<AnyEvent::MCP::Server::JSONL> manage
496 connections. L<AnyEvent::MCP::Server::Request> describes request fields,
497 replies, progress and cancellation. The same tool callback model applies
498 at both levels.
499
500 =head1 FUNCTIONS
501
502 =over
503
504 =item mcp_config %config
505
506 Adds server configuration. Repeated calls extend or replace previously declared
507 values. C<instructions> is treated as human-readable text and normalized.
508
509 Server and transport options are passed to the underlying objects.
510 Common options include:
511
512 =over
513
514 =item name => $instance
515
516 Local instance name used to select configuration and credentials. Independent
517 of the name reported to MCP clients.
518
519 =item server_name => $name
520
521 Advertised MCP server implementation name. Defaults to C<AnyEvent::MCP>.
522
523 =item server_version => $version
524
525 Advertised application version, not the MCP protocol revision. Accepts
526 an ordinary Perl version number; Simple stringifies it for MCP.
527 Defaults to the AnyEvent::MCP library version.
528
529 =item server_info => { name => $name, version => $version }
530
531 Raw MCP server identity, passed through unchanged to the underlying
532 server. Cannot be combined with C<server_name> or C<server_version>.
533
534 =item instructions => $text
535
536 Human-readable guidance for the client, normalized when configured.
537
538 =item short_instructions => $text
539
540 Optional condensed guidance for finding this server's tools. Used as the
541 advertised MCP instructions when C<auto_instructions> is enabled.
542
543 =item auto_instructions => $boolean
544
545 Enabled by default. Exposes full instructions through the generated
546 C<meta-instructions> tool while advertising the short instructions if set.
547 When short or custom instructions are provided, append a brief hint pointing
548 to the full tool description; otherwise advertise full instructions as before.
549 Disable to advertise full instructions and omit the generated tool.
550
551 =item custom_instructions => $text
552
553 Instance-specific purpose prepended to advertised instructions. Can also be
554 set through C<--custom-instructions> or C<custom_instructions> in the program
555 or instance section of the global INI configuration.
556
557 =item on_request => $callback->($server, $req)
558
559 Synchronous hook before each dispatch attempt. For example, refresh
560 database state here. This does not replace dispatch or receive a
561 completion callback, and permission retries can invoke it again.
562
563 =item on_start => $callback->($server)
564
565 Called after the transport is ready, before entering the event loop.
566 Use it for startup information or installing watchers. Stdout carries
567 protocol messages in stdio mode; print diagnostics to stderr.
568 The callback must not block.
569
570 =item idle_exit => $seconds
571
572 Optional positive idle time for spawned stdio servers. With a multibroker
573 advertising C<anyevent-mcp/multibroker> version 1, Simple requests shutdown
574 once no MCP requests have been active for that long. The broker can ignore
575 the request. An application may veto shutdown with C<on_idle_shutdown>;
576 Simple retries after the configured interval if it is declined. HTTP servers,
577 ordinary clients and servers without this option are unaffected.
578
579 =back
580
581 See L<AnyEvent::MCP::Server> for further protocol options and events.
582
583 =item mcp_permissions $object
584
585 Select the L<AnyEvent::MCP::Server::Permissions> object used by the Simple
586 starters. If never called (and no explicit C<permissions> option is supplied),
587 Simple creates one automatically, named from the program and selected instance.
588 C<mcp_permissions undef> explicitly disables the permission system:
589 tool callbacks calling C<require_permission> then fail instead of prompting.
590 Applications can still pass C<permissions> through C<mcp_config> or startup
591 arguments when needed.
592
593 The common C<--ask> option overrides the object's prompting mode, and CLI
594 automatic permission rules precede configured rules and rules originally
595 supplied with the object. Those rules are applied when starting the server,
596 not when C<mcp_permissions> is called. No permission object is created when
597 it is explicitly disabled.
598
599 =item mcp_tool $name, $description, $input_schema, $callback->($req, $args)
600
601 Registers a tool named C<$name> with C<$description> and the JSON Schema
602 hash reference C<$input_schema>. C<$description> is normalized like
603 C<instructions>.
604
605 C<$callback> receives an L<AnyEvent::MCP::Server::Request> object C<$req>
606 and its C<< $req->{args} >> hash reference as C<$args>, for convenience.
607
608 Tools complete the request with C<< $req->reply ($result) >>, or use
609 C<< $req->result ($data) >> and C<< $req->error ($message) >> for convenience.
610 Request context is available directly in
611 the hash, for example C<< $req->{tool} >> and C<< $req->{server} >>.
612
613 =item $server_options = mcp_getoptions ({ %settings }, @getopt_options)
614
615 Parse the switches documented in L</COMMON COMMAND-LINE OPTIONS> alongside
616 application-specific L<Getopt::Long> option specifications:
617
618 my $verbose;
619 my $options = mcp_getoptions (
620 {
621 usage => "Usage: $0 [OPTION]... WORKDIR\n",
622 description => "Provide example tools through MCP.",
623 option_heading => "Example options:",
624 options => [
625 ["--verbose", "Show additional diagnostics"],
626 ],
627 examples => " $0 --stdio ./project",
628 path => "/",
629 },
630 "verbose!" => \$verbose,
631 );
632 @ARGV == 1 or die "workdir required\n";
633 mcp_start %$options;
634
635 The optional leading hash accepts these help settings:
636
637 =over
638
639 =item usage => $text
640
641 Usage synopsis, including its trailing newline. A default synopsis is
642 generated when omitted.
643
644 =item description => $text
645
646 Introductory help paragraph.
647
648 =item options => [[$label, $description]...]
649
650 Application option labels and descriptions. A plain string among the
651 pairs starts a new section; a string at the beginning replaces the
652 default section heading.
653
654 =item option_heading => $text
655
656 Initial section heading, defaulting to C<Application options:>.
657
658 =item examples => $text
659
660 Preformatted examples.
661
662 =item without => [OPTION...]
663
664 Exclude selected common options by their long names, for example
665 C<< without => ["ask"] >>. The parser rejects excluded switches, and
666 C<--help> omits them. All common options are enabled by default.
667
668 =back
669
670 See L</COMMON COMMAND-LINE OPTIONS> for the full reference to shared
671 server switches. Application-specific Getopt::Long options are parsed alongside them.
672
673 These switches also appear in C<--help>; application options are passed
674 through to L<Getopt::Long>. Other leading hash settings (e.g. C<path>,
675 C<auth>, C<url_scheme>) go to C<mcp_start>. Parse positional C<@ARGV>
676 in the application; C<mcp_getoptions> deliberately does not require them
677 for help or URL discovery. Default activity logging includes asynchronous
678 tool failures and formats time, program, instance and PID. Environment
679 C<PERL_ANYEVENT_LOG> can select destinations and verbosity.
680
681 =item mcp_tool_help ($enabled, @toolsets)
682
683 If C<$enabled> is true, print the definitions registered in the supplied
684 L<AnyEvent::MCP::Server::Toolset> objects and exit. Otherwise do nothing.
685 Call after registering tools but before starting listeners for executables
686 which manage their own toolsets. Only registered tools appear.
687
688 =item mcp_log $message
689
690 Emit operator-facing activity to the configured AnyEvent::Log destination,
691 bypassing its diagnostic verbosity filter. C<--quiet> suppresses this log.
692
693 =item mcp_log_request ($server, $req, %extra)
694
695 C<on_request> callback to report tool invocations once across permission
696 retries, and log tool-error replies, including asynchronous ones. It logs
697 selected arguments rather than entire request or reply payloads. This includes
698 JSON-quoted previews of command arguments and VCS filenames (up to four,
699 80 characters each), and Vim C<ex> commands (up to 200 characters).
700 These can contain sensitive text. The Simple starters install it by default;
701 custom servers can use it too.
702 C<%extra> appends application-specific C<key=value> fields in key order.
703 Sanitize sensitive arguments before passing them.
704
705 =item mcp_start %config
706
707 Starts and runs the usual server. When C<stdio> is true or
708 C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL> is C<stdio>, uses C<mcp_stdio>.
709 Otherwise starts HTTP without printing its URL. HTTP-capable executables
710 provide C<--print-url> to retrieve the URL explicitly.
711
712 The HTTP defaults are IPv4 loopback, a stable name-derived port in
713 16384..32767, and C</> as the endpoint path.
714 Without explicit C<tokens>, one server-specific token is derived from
715 the private global secret. The selected token authenticates either its URL
716 path or an
717 C<Authorization: Bearer TOKEN> header at the clean URL returned by
718 C<< $server->url (0) >>. The corresponding executable's C<--print-url>
719 option prints the first accepted token in its URL.
720
721 C<name> selects the C<[instance NAME]> configuration section and credential.
722 It defaults to C<default>.
723 Named settings C<bind>, C<path>, C<url_host>, C<url_scheme>, C<tokens>,
724 C<tls_cert> and C<tls_key> are loaded from the global configuration.
725 Explicit C<tokens> (an ordered,
726 nonempty array reference of URL-safe strings) replaces the configured list
727 and the derived default. Every token works in the URL and bearer header.
728 An explicit C<bind> overrides the configured bind address. Other arguments
729 override these settings.
730
731 The global secret is generated automatically when needed; see
732 L<AnyEvent::MCP/GLOBAL CONFIGURATION>. No configuration is loaded for stdio.
733 Unless explicitly disabled using C<mcp_permissions undef>, Simple constructs
734 a default permission engine. C<--ask> and automatic permission rules configure
735 it along with any application-supplied permission object.
736 HTTP startup neither prints to stdout nor changes its buffering.
737 C<mcp_start> does not parse C<@ARGV>; applications parse their own options.
738 The bundled executables use C<mcp_getoptions>, which handles shared
739 options and URL discovery before starting the event loop.
740
741 =item mcp_connect %config
742
743 Run a reverse-connecting JSONL server using C<connect =E<gt>
744 "http[s]://host:port/token/alias">. HTTPS verifies the server certificate
745 and hostname. It sends the authenticated extension hello only after the
746 transport is established, then serves MCP over the socket. Refused TCP
747 connections retry every second for at most sixty attempts, and unexpected
748 established disconnects start a new retry period. Other TCP setup and TLS
749 handshake errors are reported and stop the server.
750 Calls are cancelled rather than replayed.
751 This connection has no spawned-child lifecycle authority.
752
753 =item mcp_stdio %config
754
755 Creates and runs a server over stdin/stdout. Arguments override the collected
756 configuration. Stdout buffering is disabled. On stdin EOF, pending replies
757 finish and stdout drains before
758 this function returns. Stream errors close the connection and cancel pending
759 requests.
760
761 =item mcp_http %config
762
763 Creates and runs an HTTP server. For example:
764
765 mcp_http
766 host => "127.0.0.1",
767 port => 31740,
768 path => "/mcp/",
769 tokens => ["your-long-random-secret"];
770
771 Arguments override the collected configuration. Unlike C<mcp_start>, this
772 function does not derive or supply authentication. B<Always> pass C<tokens>,
773 or explicitly pass C<auth =E<gt> "none"> when a trusted embedding layer
774 enforces access. Omitting both is an error, not an unauthenticated default.
775 It does not print a URL; use
776 C<on_start> for application-specific startup output. Neither function
777 implements OAuth or Basic authentication. See
778 L<AnyEvent::MCP::Server::HTTP/AUTHENTICATION> for details.
779
780 All three starters return when the server is closed or on C<SIGINT> or
781 C<SIGTERM>. Signal shutdown closes connections and cancels outstanding
782 requests. The signal watchers belong to the blocking call.
783
784 =back
785
786 =head1 COMMON COMMAND-LINE OPTIONS
787
788 C<mcp_getoptions> parses these options for executables using
789 L<AnyEvent::MCP::Server::Simple>. The executable's own OPTIONS section
790 documents any additional switches or differences in shared-option behavior.
791 An application can exclude specific common switches via C<without>.
792
793 =over
794
795 =item --name NAME
796
797 Select the instance (default C<default>). NAME starts with an ASCII letter,
798 digit or underscore; subsequent characters may also contain C<.>, C<@>,
799 C<+> and C<->.
800 Selects C<[instance NAME]> configuration, derived HTTP port and credentials;
801 does not change the advertised MCP implementation name.
802 Example: C<--name project>.
803
804 =item --bind ADDRESS[:PORT]
805
806 Override HTTP listening address and port. Defaults to C<127.0.0.1> and a
807 stable instance-derived port in 16384..32767. C<:31740> uses the loopback
808 address and port 31740; C<127.0.0.1:31740> specifies both.
809 Port zero requests an ephemeral port and is incompatible with
810 C<--print-url>. Ignored for stdio-only startup.
811
812 =item --tls-cert FILE
813
814 Enable opportunistic HTTPS using a PEM certificate chain. Without
815 C<--tls-key>, FILE must also contain the private key. The same listener
816 continues to accept plain HTTP.
817
818 =item --tls-key FILE
819
820 Private-key PEM file for C<--tls-cert>. If supplied alone, FILE must
821 contain both certificate chain and private key. Requires C<Net::SSLeay>.
822
823 =item --token TOKEN
824
825 Accept TOKEN for HTTP authentication. Repeat to accept multiple tokens.
826 Explicit tokens replace configured or master-secret-derived defaults.
827 Tokens work both as credential-bearing URL components and with the
828 C<Authorization: Bearer TOKEN> header.
829 Example: C<--token first --token second>.
830
831 =item -s, --set KEY=VALUE
832
833 Override one trusted configuration entry for the selected instance in this
834 process. Applied after C<[program NAME]> and C<[instance NAME]> defaults,
835 without changing the configuration file. Repeat to override multiple entries;
836 the last assignment to a key wins. Empty values suppress inherited settings,
837 and additional equals signs belong to the value. The supported keys depend
838 on the server (for example, C<executable.perl> and C<prepend-path> in
839 L<mcp-fs>). Example: C<-s executable.perl=/opt/perl/bin/perl>.
840
841 =item --stdio
842
843 Serve MCP JSONL on stdin/stdout rather than the default HTTP listener.
844 Applications with auxiliary listeners can keep those active. Cannot
845 combine explicitly with C<--connect>.
846
847 =item --connect URL
848
849 Reverse-connect to an upstream multibroker. For example:
850 C<http://host:port/TOKEN/alias> or C<https://host:port/TOKEN/alias>.
851 HTTPS requires successful certificate and hostname verification; it
852 never falls back to plaintext. Replaces the default MCP listener and
853 overrides implicit stdio selection. Cannot combine with explicit
854 C<--stdio>.
855
856 =item --custom-instructions TEXT
857
858 Override configured instance-purpose guidance describing when MCP
859 clients should use the server.
860 Example: C<--custom-instructions 'Use for build logs'>.
861
862 =item --auto-allow TOOL[,ACTION[,CAPABILITY]]
863
864 Add a process-local permission allow rule, repeatable. TOOL identifies
865 the permission-checked tool; ACTION and CAPABILITY narrow the match.
866 Missing fields and C<*> match anything.
867 Example: C<--auto-allow '*,read,workdir-read'>.
868 Rules run in command-line order before saved rules; they cannot grant
869 permissions to tools that do not check permissions.
870 See L<AnyEvent::MCP::Server::Permissions/auto_rules>.
871
872 =item --auto-deny TOOL[,ACTION[,CAPABILITY]]
873
874 Add a repeatable process-local deny rule with the same matching syntax
875 and evaluation order as C<--auto-allow>.
876 Example: C<--auto-deny '*,exec'>.
877
878 =item --ask MODE
879
880 Choose how permissions or other human input are requested: C<helper>,
881 C<tty>, C<yad>, C<mcp>, or C<auto>. C<none> immediately declines requests
882 that would otherwise require prompting; existing allow and deny rules
883 still apply. Multibroker replies to downstream forms with a standard decline.
884 The default depends on the application and its multibroker environment.
885
886 =item --quiet
887
888 Suppress routine tool activity and tool-failure logging. Warnings and
889 ordinary diagnostics remain; C<PERL_ANYEVENT_LOG> determines diagnostic
890 levels and destinations.
891
892 =item --print-url[=USER]
893
894 Print credential-bearing URLs and exit without starting a listener.
895 With managed users, prints each effective enabled user's URL, or only
896 the exact username USER. Comments identify scope-qualified principals
897 and the selected auxiliary connection user. With configured TLS, HTTPS
898 is printed before HTTP. Explicit tokens retain a single-token output
899 and cannot be combined with a user selector. Requires a predictable,
900 nonzero port and a persistent master secret for derived credentials.
901 Example: C<--name project --print-url=claude>.
902
903 =item --print-urls
904
905 Alias for C<--print-url>.
906
907 =item --tool-help
908
909 Print currently registered tool names, descriptions, argument types,
910 required fields and defaults; exit without starting the MCP listener.
911 Registered tools can depend on installed utilities and application setup.
912
913 =item --help
914
915 Print shared and application-specific command-line help; exit without
916 starting a server. Invalid options instead report usage.
917
918 =back
919
920 =cut
921
922 package AnyEvent::MCP::Server::Simple;
923
924 use common::sense;
925
926 use Exporter ();
927 use JSON::XS ();
928 use Scalar::Util qw(refaddr);
929
930 use AnyEvent ();
931 use AnyEvent::Log ();
932 use AnyEvent::MCP ();
933 use AnyEvent::MCP::Config ();
934 use AnyEvent::MCP::Server::Toolset ();
935
936 our @ISA = qw(Exporter);
937 our @EXPORT = qw(mcp_config mcp_permissions mcp_tool mcp_getoptions mcp_tool_help mcp_log mcp_log_request mcp_start mcp_stdio mcp_http mcp_connect);
938
939 my %config = (
940 auto_instructions => 1,
941 on_request => \&mcp_log_request,
942 on_connection_error => sub {
943 my ($server, $handle, $error) = @_;
944 my $info = $handle->{mcp_peer}{client_info};
945 my $name = ref $info eq "HASH" ? $info->{name} : undef;
946 my @details = (
947 "id=" . sprintf ("%x", refaddr $handle),
948 defined $handle->{mcp_remote} ? "remote=$handle->{mcp_remote}" : (),
949 defined $name && !ref $name ? "client=" . JSON::XS::encode_json (substr $name, 0, 80) : (),
950 );
951 AE::log trace => "MCP connection " . join (" ", @details) . ": $error";
952 },
953 );
954 my $toolset = AnyEvent::MCP::Server::Toolset->new;
955 my $quiet;
956
957 sub _run;
958
959 # operational messages go to the configured AnyEvent::Log destination,
960 # independently of the diagnostic verbosity filter.
961 sub mcp_log {
962 return if $quiet;
963 $AnyEvent::Log::LOG->log (info => @_);
964 }
965
966 sub mcp_log_request {
967 my ($server, $req, %extra) = @_;
968
969 return unless $req->{method} eq "tools/call";
970 return if $req->{mcp_logged}++;
971
972 my $args = $req->{args};
973 my $vcs = $req->{tool} =~ /\Avcs-(?:diff|status|log)\z/;
974 my @details;
975 for (qw(target workdir path cwd), $vcs ? "backend" : ()) {
976 push @details, "$_=" . (substr $args->{$_}, 0, 160)
977 if defined $args->{$_} && !ref $args->{$_};
978 }
979 push @details, substr +(join " ", @{$args->{ranges}}), 0, 180
980 if ref $args->{ranges} eq "ARRAY";
981 for my $key ("argv", $vcs ? "files" : ()) {
982 next unless ref $args->{$key} eq "ARRAY" && @{$args->{$key}};
983 require JSON::XS;
984 my @values = @{$args->{$key}};
985 push @details, "$key=" . join (" ", map {
986 JSON::XS::encode_json (substr $_, 0, 80) . (length $_ > 80 ? "..." : "")
987 } @values[0 .. ($#values < 3 ? $#values : 3)])
988 . (@values > 4 ? " ..." : "");
989 }
990 if ($req->{tool} eq "grep" && defined $args->{pattern} && !ref $args->{pattern}) {
991 require JSON::XS;
992 push @details, "pattern=" . JSON::XS::encode_json (substr $args->{pattern}, 0, 160)
993 . (length $args->{pattern} > 160 ? "..." : "");
994 }
995 push @details, "name=" . (substr $args->{name}, 0, 160)
996 if $req->{tool} eq "call-tool" && defined $args->{name} && !ref $args->{name};
997 if ($req->{tool} eq "ex" && defined $args->{command} && !ref $args->{command}) {
998 require JSON::XS;
999 push @details, "command=" . JSON::XS::encode_json (substr $args->{command}, 0, 200)
1000 . (length $args->{command} > 200 ? "..." : "");
1001 }
1002 if ($req->{tool} eq "path-op") {
1003 push @details, map {
1004 ($_ eq "unlink" || $_ eq "rmdir")
1005 ? ($args->{$_} ? $_ : ())
1006 : (defined $args->{$_} && !ref $args->{$_}
1007 ? "$_=" . (substr $args->{$_}, 0, 80) : ())
1008 } qw(unlink rmdir mkpath mkdir symlink truncate chmod rename);
1009 }
1010 for my $key (sort keys %extra) {
1011 next unless defined $extra{$key} && !ref $extra{$key};
1012 push @details, "$key=" . (substr $extra{$key}, 0, 160);
1013 }
1014
1015 (my $message = join " ", $req->{tool}, @details) =~ s/[\r\n]/ /g;
1016 mcp_log $message;
1017
1018 my $reply = $req->{on_reply};
1019 $req->{on_reply} = sub {
1020 my ($req, $result) = @_;
1021
1022 if ($result->{isError}) {
1023 my $message = ref $result->{content} eq "ARRAY"
1024 && ref $result->{content}[0] eq "HASH"
1025 ? $result->{content}[0]{text} // "unknown error" : "unknown error";
1026 $message =~ s/\n.*//s;
1027 mcp_log "$req->{tool} failed: " . (substr $message, 0, 240);
1028 }
1029 $reply->(@_);
1030 };
1031 }
1032
1033 sub _mcp_log_init {
1034 my ($name, $silent) = @_;
1035
1036 $quiet = $silent;
1037 my $identity = AnyEvent::MCP::Config::program_name () . "/$name" . "[$$]";
1038
1039 no warnings 'redefine';
1040 *AnyEvent::Log::default_format = sub ($$$$) {
1041 my ($when, $ctx, $level, $message) = @_;
1042 my $stamp = substr +(AnyEvent::Log::format_time $when), 8, 14;
1043 my $source = $ctx == $AnyEvent::Log::LOG ? "" : "$AnyEvent::Log::LEVEL2STR[$level] $ctx->[0]: ";
1044 join "", map "$stamp $identity $source$_\n", split /\n/, $message;
1045 };
1046 }
1047
1048 sub mcp_config {
1049 my %kv = @_;
1050
1051 $kv{instructions} = AnyEvent::MCP::_clean_text $kv{instructions}
1052 if exists $kv{instructions};
1053
1054 %config = (%config, %kv);
1055 }
1056
1057 sub mcp_permissions {
1058 @_ == 1 or die "mcp_permissions expects one argument (object or undef)\n";
1059 $config{permissions} = $_[0];
1060 }
1061
1062 sub mcp_tool {
1063 $toolset->tool (@_)
1064 }
1065
1066 # format a JSON Schema type for command-line tool help.
1067 sub _tool_type {
1068 my ($schema) = @_;
1069 return "any" unless ref $schema eq "HASH";
1070 for my $key (qw(anyOf oneOf)) {
1071 return join "|", map _tool_type ($_), @{$schema->{$key}}
1072 if ref $schema->{$key} eq "ARRAY";
1073 }
1074 my $type = $schema->{type} // "any";
1075 $type = join "|", @$type if ref $type eq "ARRAY";
1076 $type = "array[" . _tool_type ($schema->{items}) . "]" if $type eq "array";
1077 $type
1078 }
1079
1080 sub mcp_tool_help {
1081 my ($enabled, @toolsets) = @_;
1082 return unless $enabled;
1083
1084 my $json = AnyEvent::MCP::json_class ()->new->canonical;
1085 my @tools = map @{$_->list}, @toolsets;
1086 for my $tool (@tools) {
1087 print "$tool->{name}\n";
1088 (my $description = $tool->{description} // "") =~ s/\n/\n /g;
1089 print " $description\n" if length $description;
1090
1091 my $schema = $tool->{inputSchema} || {};
1092 my $properties = $schema->{properties} || {};
1093 my %required = map { $_ => 1 } @{$schema->{required} || []};
1094 for my $name (sort keys %$properties) {
1095 my $property = $properties->{$name};
1096 my $type = _tool_type ($property);
1097 my $status = $required{$name} ? "required" : "optional";
1098 my @extras;
1099 push @extras, "default: " . $json->encode ($property->{default})
1100 if exists $property->{default};
1101 push @extras, "values: " . join (", ", @{$property->{enum}})
1102 if ref $property->{enum} eq "ARRAY";
1103 print " $name $type $status", (@extras ? " (" . join ("; ", @extras) . ")" : ""), "\n";
1104 if (length ($property->{description} // "")) {
1105 (my $detail = $property->{description}) =~ s/\n/\n /g;
1106 print " $detail\n";
1107 }
1108 }
1109 print "\n";
1110 }
1111 print "No tools registered.\n" unless @tools;
1112 exit;
1113 }
1114
1115 sub _mcp_help_row {
1116 my ($option, $description) = @_;
1117
1118 my $start = " $option";
1119 my $margin = " " x 34;
1120 my $out = length $start < 34 ? "" : "$start\n";
1121 my $line = length $start < 34 ? $start . (substr $margin, length $start) : $margin;
1122
1123 for my $word (split /\s+/, $description) {
1124 if (length $line > 34 && (length $line) + 1 + length $word > 79) {
1125 $out .= "$line\n";
1126 $line = $margin;
1127 }
1128
1129 $line .= " " if length $line > 34;
1130 $line .= $word;
1131 }
1132
1133 "$out$line\n"
1134 }
1135
1136 sub _mcp_help {
1137 my ($usage, $description, $options, $option_heading, $examples, $without) = @_;
1138
1139 $usage =~ s/\n*\z/\n/;
1140 my $help = $usage;
1141 $help .= "\n$description\n" if defined $description && length $description;
1142
1143 $help .= "\nCommon options:\n";
1144 my @common = (
1145 ["--name NAME", "Select named configuration (instance defaults to 'default')"],
1146 ["--bind ADDRESS[:PORT]", "Override the HTTP listen address or port"],
1147 ["--tls-cert FILE", "PEM certificate (and optionally key) for HTTPS"],
1148 ["--tls-key FILE", "PEM private key (or combined key/certificate PEM)"],
1149 ["--token TOKEN", "Set an accepted token (repeatable; replaces defaults)"],
1150 ["-s, --set KEY=VALUE", "Override a trusted instance setting (repeatable)"],
1151 ["--stdio", "Serve MCP on stdin/stdout; other listeners may remain"],
1152 ["--connect URL", "Connect to a multibroker at http[s]://host:port/token/alias"],
1153 ["--custom-instructions TEXT", "Describe when this server instance should be used"],
1154 ["--auto-allow TOOL[,ACTION[,CAPABILITY]]", "Add an ordered automatic allow rule (repeatable)"],
1155 ["--auto-deny TOOL[,ACTION[,CAPABILITY]]", "Add an ordered automatic deny rule (repeatable)"],
1156 ["--ask MODE", "Prompt via helper, tty, yad, mcp, auto; none declines without prompting"],
1157 ["--quiet", "Suppress routine tool activity and tool-failure logging"],
1158 ["--print-url[=USER] (--print-urls)", "Print enabled user URLs, or one exact user"],
1159 ["--tool-help", "Print registered tool descriptions and arguments, then exit"],
1160 ["--help", "Display this help and exit"],
1161 );
1162 $help .= _mcp_help_row (@$_) for grep {
1163 my ($name) = $_->[0] =~ /--([a-z-]+)/;
1164 !$without->{$name}
1165 } @common;
1166
1167 if ($options && @$options) {
1168 $help .= "\n" . ($option_heading // "Application options:") . "\n"
1169 if ref $options->[0];
1170 for my $option (@$options) {
1171 if (ref $option) {
1172 $help .= _mcp_help_row (@$option);
1173 } else {
1174 $help .= "\n" unless $help =~ /\n\n\z/;
1175 $help .= "$option\n";
1176 }
1177 }
1178 }
1179 $help .= "\nExamples:\n$examples\n"
1180 if defined $examples && length $examples;
1181 $help .= "\nNamed users and URLs:\n"
1182 . " anyevent-mcp create-user chatgpt\n"
1183 . " $0 --print-urls # list URLs for all enabled users\n"
1184 . " $0 --print-url=chatgpt # select one user\n"
1185 . "A URL marked (connect) is the one used for browser, Vim, or reverse connections.\n"
1186 . "Users inherit from global, program, then instance; server restart is needed\n"
1187 . "after changing user definitions.\n";
1188
1189 $help
1190 }
1191
1192 sub mcp_getoptions {
1193 require Getopt::Long;
1194
1195 my %settings = %{ref $_[0] eq "HASH" ? shift : {}};
1196 my $usage = delete $settings{usage}
1197 // "Usage: $0 [OPTION]... [ARG]...\n";
1198 my @help = delete @settings{qw(description options option_heading examples)};
1199 my %without = map { $_ => 1 } @{delete $settings{without} || []};
1200 my %url = (%config, %settings);
1201 delete @url{qw(name bind stdio)};
1202 my @auto_rules;
1203 my %set;
1204
1205 my @common = (
1206 AnyEvent::MCP::Config::http_getopt (\my $name, \my $bind, \my $print_url, \my @tokens),
1207 "tls-cert=s" => \my $tls_cert,
1208 "tls-key=s" => \my $tls_key,
1209 "stdio" => \my $stdio,
1210 "connect=s" => \my $connect,
1211 "custom-instructions=s" => \my $custom_instructions,
1212 "auto-allow=s" => sub { push @auto_rules, ["allow", $_[1]] },
1213 "auto-deny=s" => sub { push @auto_rules, ["deny", $_[1]] },
1214 "set|s=s" => sub {
1215 my ($key, $value) = $_[1] =~ /\A([^\s=]+)=(.*)\z/;
1216 defined $value or die "Invalid --set KEY=VALUE\n";
1217 $set{$key} = $value;
1218 },
1219 "tool-help" => \my $tool_help,
1220 "quiet" => \my $silent,
1221 "ask=s" => \my $ask,
1222 "help" => \my $help,
1223 );
1224 my @enabled;
1225 my %known;
1226 while (@common) {
1227 my ($option, $target) = splice @common, 0, 2;
1228 my ($name) = $option =~ /\A([a-z-]+)/;
1229 $known{$name} = 1;
1230 push @enabled, $option => $target unless $without{$name};
1231 }
1232 die "Unknown common option: " . join (", ", sort grep !$known{$_}, keys %without) . "\n"
1233 if grep !$known{$_}, keys %without;
1234 Getopt::Long::GetOptions (
1235 @enabled,
1236 @_,
1237 ) or die $usage;
1238 die "--ask must be helper, tty, yad, mcp, auto, or none\n"
1239 if defined $ask && $ask !~ /\A(?:helper|tty|yad|mcp|auto|none)\z/;
1240
1241 if ($help) {
1242 print _mcp_help $usage, @help, \%without;
1243 exit;
1244 }
1245
1246 $name = AnyEvent::MCP::instance_name ($name // $settings{name} // $config{name});
1247 if (%set) {
1248 my $instance = (AnyEvent::MCP::Config::global_config)->{section}{"instance $name"} ||= {};
1249 %$instance = (%$instance, %set);
1250 }
1251
1252 _mcp_log_init ($name, $silent);
1253 my %server = (
1254 %settings,
1255 name => $name,
1256 (defined $bind ? (bind => $bind) : ()),
1257 (defined $tls_cert ? (tls_cert => $tls_cert) : ()),
1258 (defined $tls_key ? (tls_key => $tls_key) : ()),
1259 (@tokens ? (tokens => \@tokens) : ()),
1260 ($stdio ? (stdio => 1) : ()),
1261 (defined $connect ? (connect => $connect) : ()),
1262 (defined $ask ? (ask => $ask) : ()),
1263 (defined $custom_instructions ? (custom_instructions => $custom_instructions) : ()),
1264 (@auto_rules ? (auto_rules => \@auto_rules) : ()),
1265 ($tool_help ? (tool_help => 1) : ()),
1266 );
1267
1268 if (defined $print_url) {
1269 my %options = (
1270 %url,
1271 (defined $tls_cert ? (tls_cert => $tls_cert) : ()),
1272 (defined $tls_key ? (tls_key => $tls_key) : ()),
1273 (@tokens ? (tokens => \@tokens) : ()),
1274 );
1275 my $global = AnyEvent::MCP::Config::global_config;
1276 $options{path} //= $global->{section}{"instance $name"}{path}
1277 // $global->{section}{"program " . AnyEvent::MCP::Config::program_name ()}{path} // "/";
1278 if (($options{auth} || "") eq "none") {
1279 !exists $options{tokens} or die "Cannot combine auth => none with tokens\n";
1280 }
1281 my $selected_bind = defined $bind ? $bind : ($settings{bind} // $config{bind});
1282 my %http = ($global->http_settings ($name, $selected_bind), %options);
1283 my @schemes = defined $http{tls_cert} || defined $http{tls_key}
1284 ? ("https", "http") : ($http{url_scheme} || "http");
1285 my $managed = ($http{auth} || "") ne "none"
1286 && !exists $options{tokens}
1287 && !exists $global->{section}{"instance $name"}{tokens}
1288 && !exists $global->{section}{"program " . AnyEvent::MCP::Config::program_name ()}{tokens};
1289
1290 if ($managed) {
1291 my $saved = (ref $global)->new (file => $global->{file})->load;
1292 $saved->{section}{global}{secret} eq $global->{section}{global}{secret}
1293 or die "MCP master secret is not persistently stored in $global->{file}\n";
1294 my $credentials = $global->user_credentials ($name);
1295 if (length $print_url) {
1296 @$credentials = grep $_->[0] eq $print_url, @$credentials;
1297 @$credentials or die "User '$print_url' is not enabled\n";
1298 }
1299 my $connect = $global->connect_user ($name);
1300 for my $entry (@$credentials) {
1301 my ($user, $principal, $token) = @$entry;
1302 for my $scheme (@schemes) {
1303 print $global->http_url ($name, $selected_bind, %options,
1304 tokens => [$token], url_scheme => $scheme),
1305 " # user $principal", ($user eq $connect ? " (connect)" : ""), "\n";
1306 }
1307 }
1308 } else {
1309 length $print_url
1310 and die "Cannot select a managed user with explicit tokens or auth=none\n";
1311 for my $scheme (@schemes) {
1312 print $global->http_url ($name, $selected_bind, %options, url_scheme => $scheme), "\n";
1313 }
1314 }
1315 exit;
1316 }
1317
1318 \%server
1319 }
1320
1321 sub _permission_options {
1322 my %kv = @_;
1323
1324 my $rules = delete $kv{auto_rules};
1325 my $ask = delete $kv{ask};
1326 unless (exists $kv{permissions}) {
1327 require AnyEvent::MCP::Server::Permissions;
1328 $kv{permissions} = AnyEvent::MCP::Server::Permissions->new (
1329 name => AnyEvent::MCP::Config::program_name () . "-" . AnyEvent::MCP::instance_name ($kv{name}),
1330 );
1331 }
1332 if (my $permissions = $kv{permissions}) {
1333 $permissions->{ask} = $ask if defined $ask;
1334 $permissions->auto_rules ($rules, 1) if $rules && @$rules;
1335 }
1336
1337 %kv
1338 }
1339
1340 sub _server_options {
1341 my %kv = @_;
1342
1343 if (exists $kv{server_name} || exists $kv{server_version}) {
1344 !exists $kv{server_info}
1345 or die "Cannot combine server_info with server_name/server_version\n";
1346
1347 my $name = delete $kv{server_name};
1348 my $version = delete $kv{server_version};
1349 $kv{server_info} = {
1350 name => defined $name ? $name : "AnyEvent::MCP",
1351 version => defined $version ? "$version" : "$AnyEvent::MCP::VERSION",
1352 };
1353 }
1354
1355 %kv
1356 }
1357
1358 sub mcp_start {
1359 my %kv = (%config, @_);
1360 mcp_tool_help (delete $kv{tool_help}, @{$kv{toolsets} || [$toolset]});
1361 my $name = AnyEvent::MCP::instance_name ($kv{name});
1362 my $config_file = AnyEvent::MCP::Config::_file ();
1363 my $global = -e $config_file ? AnyEvent::MCP::Config::global_config : undef;
1364 my %ini = $global ? (
1365 %{$global->{section}{"program " . AnyEvent::MCP::Config::program_name ()} || {}},
1366 %{$global->{section}{"instance $name"} || {}},
1367 ) : ();
1368 if ($global) {
1369 my @rules = $global->auto_rules ($name);
1370 $kv{auto_rules} = [@{$kv{auto_rules} || []}, @rules] if @rules;
1371 }
1372 $kv{custom_instructions} = $ini{custom_instructions}
1373 if !exists $kv{custom_instructions} && exists $ini{custom_instructions};
1374 %kv = _permission_options (%kv);
1375 my $stdio = delete $kv{stdio};
1376 my $listen = delete $kv{listen};
1377 my $connect = delete $kv{connect};
1378 !($stdio && defined $connect) or die "Cannot combine --stdio and --connect\n";
1379 $stdio ||= !defined $connect && $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio";
1380
1381 $kv{name} = $name;
1382
1383 if (!$listen) {
1384 return mcp_connect (%kv, connect => $connect) if defined $connect;
1385 return mcp_stdio (%kv) if $stdio;
1386 }
1387
1388 my %http = ($global ||= AnyEvent::MCP::Config::global_config)->http_settings ($name, delete $kv{bind});
1389 delete @http{qw(tokens connect_tokens token_users)} if ($kv{auth} || "") eq "none";
1390 my %options = (path => "/", %http, %kv);
1391 if (exists $kv{tokens}) {
1392 $options{connect_tokens} = $kv{tokens};
1393 delete $options{token_users};
1394 }
1395
1396 if ($stdio || defined $connect) {
1397 require AnyEvent::MCP::Server::HTTP;
1398 my $listener = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], _server_options (%options));
1399 $listener->listen;
1400 my $result = defined $connect
1401 ? mcp_connect (%kv, connect => $connect)
1402 : mcp_stdio (%kv);
1403 $listener->close;
1404 return $result
1405 }
1406
1407 mcp_http (%options)
1408 }
1409
1410 sub mcp_connect {
1411 require AnyEvent::MCP::Server::Connect;
1412
1413 my %kv = _server_options (%config, @_);
1414 mcp_tool_help (delete $kv{tool_help}, @{$kv{toolsets} || [$toolset]});
1415 my $server = AnyEvent::MCP::Server::Connect->new (toolsets => [$toolset], _permission_options (%kv));
1416 $server->start;
1417 _run ($server, 1)
1418 }
1419
1420 sub mcp_stdio {
1421 require AnyEvent::MCP::Server::JSONL;
1422
1423 my %kv = _permission_options (_server_options (%config, @_));
1424 mcp_tool_help (delete $kv{tool_help}, @{$kv{toolsets} || [$toolset]});
1425 my $idle_exit = delete $kv{idle_exit};
1426 binmode STDIN;
1427 binmode STDOUT;
1428 my $selected = select STDOUT;
1429 $| = 1;
1430 select $selected;
1431
1432 my $server = AnyEvent::MCP::Server::JSONL->new (toolsets => [$toolset], %kv);
1433 $server->accept (\*STDIN, \*STDOUT);
1434
1435 if ($idle_exit) {
1436 $idle_exit =~ /\A[0-9]+(?:\.[0-9]+)?\z/ && $idle_exit > 0
1437 or die "invalid idle_exit seconds\n";
1438 if ($ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio") {
1439 my ($timer, $check);
1440 $check = sub {
1441 return if $server->{idle_exit_requested};
1442 if (grep $_->{mcp_active}, values %{$server->{connections} || {}}) {
1443 $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
1444 } else {
1445 $server->request_idle_exit;
1446 }
1447 };
1448 my $schedule = sub {
1449 $timer = AnyEvent->timer (after => $idle_exit, cb => $check);
1450 };
1451 my $on_idle_shutdown = $server->{on_idle_shutdown};
1452 $server->{on_idle_shutdown} = sub {
1453 my $allowed = !$on_idle_shutdown || $on_idle_shutdown->(@_);
1454 $schedule->() unless $allowed;
1455 $allowed
1456 };
1457 my $on_request = $server->{on_request};
1458 $server->{on_request} = sub {
1459 my ($server, $req) = @_;
1460
1461 if ($req->{method} ne "anyevent-mcp/shutdown") {
1462 $server->{idle_exit_requested} = 0;
1463 $schedule->();
1464 my $reply = $req->{on_reply};
1465 $req->{on_reply} = sub {
1466 $reply->(@_);
1467 $schedule->() unless $server->{_idle_shutting_down};
1468 };
1469 }
1470 $on_request->(@_) if $on_request;
1471 };
1472 my $on_close = $server->{on_close};
1473 $server->{on_close} = sub {
1474 undef $timer;
1475 $on_close->(@_) if $on_close;
1476 };
1477 $schedule->();
1478 }
1479 }
1480 _run $server
1481 }
1482
1483 sub mcp_http {
1484 require AnyEvent::MCP::Server::HTTP;
1485
1486 my %kv = _permission_options (_server_options (%config, @_));
1487 mcp_tool_help (delete $kv{tool_help}, @{$kv{toolsets} || [$toolset]});
1488 my $server = AnyEvent::MCP::Server::HTTP->new (toolsets => [$toolset], %kv);
1489 $server->listen;
1490 _run $server
1491 }
1492
1493 sub _run {
1494 my ($server, $keep_alive) = @_;
1495
1496 my $cv = AnyEvent->condvar;
1497 my $stopped;
1498 my $stop = sub { $cv->send unless $stopped++ };
1499 my $on_close = $server->{on_close};
1500 $server->{on_close} = sub {
1501 $on_close->(@_) if $on_close;
1502 $stop->();
1503 };
1504
1505 if (!$keep_alive && $server->isa ("AnyEvent::MCP::Server::JSONL")) {
1506 my $on_connection_close = $server->{on_connection_close};
1507 $server->{on_connection_close} = sub {
1508 $on_connection_close->(@_) if $on_connection_close;
1509 $stop->();
1510 };
1511 }
1512
1513 my @signals = map {
1514 AnyEvent->signal (signal => $_, cb => sub { $server->close })
1515 } qw(INT TERM);
1516
1517 my $ok = eval {
1518 $server->event (start =>);
1519 $cv->recv;
1520 1
1521 };
1522 my $error = $@;
1523 $server->close;
1524 die $error unless $ok;
1525 return
1526 }
1527
1528 1