ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP.pm
Revision: 1.8
Committed: Fri Oct 9 06:18:51 2026 UTC (4 weeks, 6 days ago) by root
Branch: MAIN
Changes since 1.7: +25 -0 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP - Model Context Protocol client/server support
4    
5     =head1 SYNOPSIS
6    
7 root 1.6 use AnyEvent::MCP::Server::Simple ();
8 root 1.1
9     mcp_tool ...
10 root 1.3 mcp_start;
11 root 1.1
12     or use the object interface directly:
13    
14 root 1.6 use AnyEvent::MCP::Server ();
15 root 1.1
16     my $server = AnyEvent::MCP::Server->new (...);
17    
18     =head1 DESCRIPTION
19    
20     AnyEvent::MCP provides event-driven Model Context Protocol client and server
21     support.
22    
23     The usual way to implement a small MCP server is
24     L<AnyEvent::MCP::Server::Simple>, which provides a function-based interface and
25     creates the underlying server objects when a transport such as stdio or HTTP is
26     started.
27    
28     The lower-level object interfaces are intended for applications that need to
29     manage MCP connections, servers or reusable tool registries directly.
30    
31     =head2 MODULES
32    
33 root 1.7 =over
34 root 1.1
35     =item L<AnyEvent::MCP::Server::Simple>
36    
37     Function-based interface for implementing ordinary MCP servers.
38    
39     =item L<AnyEvent::MCP::Server>
40    
41     Object-oriented MCP server interface.
42    
43     =item L<AnyEvent::MCP::Client>
44    
45     Object-oriented MCP client interface.
46    
47     =item L<AnyEvent::MCP::Server::Toolset>
48    
49     Reusable registry of MCP tool declarations.
50    
51     =item L<AnyEvent::MCP::Server::Permissions>
52    
53     Permission and capability handling for MCP servers.
54    
55     =back
56    
57 root 1.4 C<mcp-multibroker> combines downstream HTTP and stdio servers under tool-name
58     prefixes. It uses the same client, server and permission helper facilities,
59     including modern input forwarding and local permission prompts for legacy
60     callers. Run C<mcp-multibroker --help> for command-line syntax; its manual
61     describes authentication and configuration.
62    
63 root 1.1 The interfaces are still being designed; this documentation is intentionally
64     only an overview for now.
65    
66 root 1.2 =head1 GLOBAL CONFIGURATION
67    
68     The shared per-user configuration is C<$XDG_CONFIG_HOME/anyevent-mcp/global.ini>,
69     or C<$HOME/.config/anyevent-mcp/global.ini> when XDG_CONFIG_HOME is unset.
70     L<AnyEvent::MCP::Config> loads and holds it; loading this module does not access
71     configuration files.
72    
73     The file is UTF-8 and uses sections with single-line C<key = value> pairs.
74     Values are unquoted strings. Blank lines and whole-line comments beginning
75     with C<#> or C<;> are ignored; inline comment characters remain part of the
76     value. Later occurrences of a key replace earlier ones in the same section.
77    
78     [global]
79     secret = ...128 hexadecimal digits...
80    
81     [server example]
82 root 1.3 port = 31740
83     url = https://example.net:31740/{cookie}/
84 root 1.2
85     [mcp-fs]
86     utility = /path/to/utility
87    
88     Section names are unrestricted. Application-specific sections, including
89     exports, utility paths and user settings, are documented by their applications.
90    
91     C<[global]> holds C<secret>, a 64-byte master key encoded as 128 hexadecimal
92     digits. The file must be owned by the current user, with no group or other
93     permissions. A missing secret is generated automatically on first configuration
94     access and saved when possible. Existing secrets are never replaced implicitly.
95     An invalid secret is an error.
96    
97     If saving fails, Config warns and retains the secret in memory. Credentials
98     then remain stable only for that process; other processes cannot derive the
99     same credentials. If randomness is unavailable, supply a securely generated
100     secret in the file.
101    
102     C<[server NAME]> holds named-server settings. Its C<url> is a complete template;
103     C<{cookie}> is replaced with the cookie derived for that server and optional
104     username. Scheme, host, port and cookie placement are explicit in the template.
105 root 1.3 HTTP servers with an explicit C<name> load C<host>, C<port>, C<path>,
106     C<url_host>, C<url_scheme>, C<cookie>, C<bearer> and C<users> from this section
107     as defaults. C<users> is a comma-separated allowlist whose Basic passwords
108     are derived for the named server. An explicit password hash can instead be
109     passed to the constructor. C<url> is a template for the utility program; it
110     does not set the listener's address.
111    
112     C<mcp_start> chooses an application name and supplies cookie/bearer
113     authentication by default. Explicit authentication settings replace this
114     default. Stdio startup does not access global configuration.
115    
116     C<[client NAME]> supplies HTTP client defaults: C<url>, C<bearer>, C<username>,
117     C<password> and C<timeout>. Clients consult it only when explicitly given a
118     C<name>. An unnamed client or HTTP server does not read global configuration.
119     Explicit constructor arguments take precedence over named defaults.
120    
121     =head1 ENVIRONMENT
122    
123     Project-specific environment variables use the C<PERL_ANYEVENT_MCP_> prefix.
124     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio> selects stdio in Simple.
125     C<PERL_ANYEVENT_MCP_MULTIBROKER_ALIAS> supplies the default application name.
126     C<PERL_ANYEVENT_MCP_PERMISSION_HELPER> selects the external asking executable.
127     Standard C<HOME> and C<XDG_CONFIG_HOME> keep their standard names.
128 root 1.2
129     =head1 FUNCTIONS
130    
131 root 1.6 =over
132    
133 root 1.8 =item $bytes = AnyEvent::MCP::utf8enc $text
134    
135     Return a UTF-8 byte string for C<$text>, leaving the argument unchanged.
136    
137     =item $text = AnyEvent::MCP::utf8dec $bytes
138    
139     Decode a UTF-8 byte string into characters, dying on invalid UTF-8.
140     The argument is left unchanged. For intentional in-place conversions,
141     use Perl's C<utf8::encode> or C<utf8::decode> directly.
142    
143 root 1.6 =item $bytes = AnyEvent::MCP::random_bytes $length
144 root 1.2
145     Returns exactly C<$length> random bytes. Tries C</dev/urandom>, then an external
146     C<openssl rand> command. No additional random-number module is required.
147     Dies if neither source supplies the requested bytes successfully.
148     C<$length> must be a nonnegative integer; zero returns an empty string.
149    
150 root 1.6 =back
151    
152     =head2 PATCH UTILITIES
153    
154 root 1.7 Since applying patches to files is a common task for MCP servers, the
155 root 1.6 following utility functions can be used to ease that job. Together, they
156 root 1.7 define a common patch format (unified diff or a simplified version of it,
157     with fewer requirements to make it easier for the LLM to generate), helpers
158 root 1.6 to apply the patch and a helper to verify if the patch was applied
159     with sufficient precision.
160    
161 root 1.7 Use C<apply_udiff> to normalize, apply and validate a patch in one operation.
162     It writes a separate output file; the caller checks freshness and metadata
163     and commits the result.
164    
165     For an application-specific applier, the workflow is:
166 root 1.6
167     =over
168    
169     =item 1. AnyEvent::MCP::normalize_udiff
170    
171 root 1.7 Returns a unified diff for GNU patch and the permitted hunk offsets.
172    
173     =item 2. Apply the normalized diff
174 root 1.6
175 root 1.7 Run GNU patch with fuzz zero and capture its verbose output.
176 root 1.6
177 root 1.7 =item 3. AnyEvent::MCP::check_patch_output
178    
179     Checks the process status and output against the permitted offsets.
180     C<parse_patch_output> is available separately when only parsing is needed.
181 root 1.5
182 root 1.6 =back
183    
184 root 1.5 =head3 Input format
185    
186 root 1.6 The patch format accepted by normalize_udiff is an extension to the
187     (essentially undocumented) unified diff format.
188    
189 root 1.5 In a hunk header, only the old start C<-N> matters. It is the old-file line
190     of the first hunk body line, including context, not necessarily the first
191     changed line. Counts and the new-file position are ignored and recomputed.
192     Both ordinary headers and the abbreviated form are accepted:
193    
194     @@ -42,99 +123,99 @@
195     @@ -42 @@
196    
197     Optional text after the closing C<@@> is preserved. File headers and other
198     text before the first hunk are preserved; a diff must describe one file.
199    
200     Hunk body lines use the usual prefixes: a space for unchanged context,
201     C<-> for removed lines and C<+> for added lines. The
202     C<\ No newline at end of file> marker is preserved and counts neither side.
203     Trailing empty lines outside the final hunk are discarded.
204    
205 root 1.7 Pure insertion is permitted only at the beginning, using old start C<0>.
206     For example, C<@@ -0 @@> with one added line becomes C<@@ -0,0 +1,1 @@>.
207     An empty old-side range names the line before the insertion: C<-0,0>
208     means before line 1, whereas C<-1,0> would mean after line 1 and is rejected
209     here. Nonempty ranges use one-based line numbers.
210    
211 root 1.5 An insertion elsewhere needs old-side context. Empty hunks are rejected.
212     New positions include the cumulative line-count changes of preceding hunks.
213    
214     =head3 Applying the result
215    
216 root 1.6 The C<normalize_udiff> function does not apply the patch, check file
217     contents or guard against concurrent edits. The caller must supply those
218     checks, if required.
219 root 1.5
220     Context must match exactly: apply with fuzz zero. For each hunk,
221     C<$max_offset> permits relocation by at most the number of distinct old-side
222     lines minus one, or zero for a pure insertion. Repeated old-side lines do
223     not increase this allowance. The applier must enforce these limits; an
224     unrestricted search for matching context can select the wrong occurrence.
225    
226     For live Vim buffers, also require the expected changedtick to match.
227     File-based users need an equivalent check before replacing the file.
228    
229 root 1.6 To work around GNU patch's handling of asymmetric context, the
230     first context line of each hunk is rewritten as an identity
231     removal/addition. The line contents and old/new line counts are
232     unchanged. This is normalization for the patch applier, not a relaxation
233     of context matching.
234 root 1.5
235 root 1.6 =over
236    
237     =item ($diff, $max_offset) = AnyEvent::MCP::normalize_udiff $diff
238    
239     Normalizes a relaxed, single-file unified diff into ordinary unified diff
240     syntax. Returns the normalized C<$diff> and an array reference C<$max_offset>
241     containing the permitted whole-hunk relocation for each hunk, in order.
242     Dies on malformed hunks or when no hunks are found.
243    
244     Here is a suitable description for the diff format, e.g. in a tool
245     definition:
246    
247     Apply a unified diff to one regular file. Hunk counts are recomputed,
248     fuzz is disabled, whole-hunk relocation is limited per hunk by distinct
249     old-side lines. Pure insertion is allowed only at the beginning;
250     elsewhere include old-side context.
251    
252     =item $cv = AnyEvent::MCP::apply_udiff $input, $output, $diff
253    
254     Asynchronously applies the relaxed C<$diff> to the input file C<$input>,
255     writing a separate output file C<$output>. C<$diff> must contain bytes;
256     callers working with characters must encode them for the file themselves.
257    
258     Normalizes the diff, runs GNU patch using C<AnyEvent::Util::run_cmd>, and
259     validates its verbose output with C<check_patch_output>. The command uses
260     batch/forward mode, binary mode and fuzz zero. No shell is involved.
261    
262     C<$output> must not exist. It is reserved with mode 0600; patch runs in its
263     directory so incidental temporary files stay there. Input and output paths
264     are made absolute before starting the command. These must be trusted paths
265     in a directory controlled by the caller.
266    
267     Returns a condition variable. Its C<recv> returns the validated hunk array
268     reference, or throws on command/application failure. Setup failures throw
269     synchronously. Failed or partial output is removed. C<$input> is unchanged.
270    
271     The caller owns committing the successful output and removing it when no
272     longer needed. Freshness checks, metadata preservation, atomic replacement
273     and application-specific permissions also belong to the caller.
274    
275     For your reference, the exact options used are:
276    
277     --verbose --batch --forward --binary --fuzz=0
278     --no-backup-if-mismatch
279     --reject-file=- --output=... -- <inputfile>
280    
281     =item $parsed = AnyEvent::MCP::parse_patch_output $output
282 root 1.5
283     Parses C<LC_ALL=C> GNU patch C<--verbose> output without judging whether
284     the application is acceptable. C<$parsed> has two fields:
285    
286 root 1.6 =over
287 root 1.5
288     =item hunks => [$hunk...]
289    
290     Hunk records in output order. Each record contains C<hunk> (the one-based
291     number), C<success>, C<line> (the reported application line), C<fuzz> and
292     the signed C<offset>. Omitted fuzz and offset are zero. Failed and ignored
293     hunks have false C<success>.
294    
295     =item unparsed => [$line...]
296    
297     Lines not recognized as hunk results or GNU patch's verbose framing.
298     The parser preserves them for callers to interpret or reject.
299    
300     =back
301    
302     Use verbose output: ordinary patch output omits exact-position successes,
303     so it cannot account for every hunk.
304    
305 root 1.6 =item $hunks = AnyEvent::MCP::check_patch_output $status, $output, [$max_offset...]
306 root 1.5
307     Parses C<$output> and validates a complete application. C<$status> is the
308     raw process wait status, as returned by C<waitpid> or
309     C<AnyEvent::Util::run_cmd>, not just the exit code.
310    
311     Requires status zero, no unrecognized output, exactly the expected hunks
312     in numbered order, successful application, fuzz zero and each signed offset
313     within its corresponding absolute limit. C<[$max_offset...]> is the array
314     reference returned by C<normalize_udiff>. At least one hunk is required.
315    
316     Returns the parsed hunk array reference on success; otherwise dies with a
317     diagnostic. This is also suitable for callers running patch inside their
318     own sandbox.
319    
320 root 1.6 =back
321 root 1.5
322 root 1.1 =cut
323    
324     package AnyEvent::MCP;
325    
326     use common::sense;
327     use Digest::SHA ();
328    
329     our $VERSION = '0.0';
330    
331 root 1.8 sub utf8enc($) {
332     my $text = shift;
333    
334     utf8::encode $text;
335     $text
336     }
337    
338     sub utf8dec($) {
339     my $bytes = shift;
340    
341     utf8::decode $bytes
342     or die "invalid UTF-8\n";
343     $bytes
344     }
345    
346 root 1.2 sub random_bytes($) {
347     my ($length) = @_;
348    
349     die "Invalid random byte count\n"
350     unless defined $length && $length =~ /\A[0-9]+\z/;
351     return "" unless $length;
352    
353     my $bytes = "";
354    
355     if (open my $fh, "<", "/dev/urandom") {
356     binmode $fh;
357     while (length $bytes < $length) {
358     read ($fh, $bytes, $length - length ($bytes), length $bytes)
359     or last;
360     }
361     close $fh;
362    
363     return $bytes if length $bytes == $length;
364     }
365    
366     if (open my $fh, "-|", "openssl", "rand", $length) {
367     binmode $fh;
368     local $/;
369     $bytes = <$fh>;
370     my $success = close $fh;
371    
372     return $bytes
373     if $success && defined $bytes && length $bytes == $length;
374     }
375    
376     die "Cannot obtain $length random bytes from /dev/urandom or openssl rand\n";
377     }
378    
379 root 1.1 sub hkdf_sha512_expand($$$) {
380     my ($prk, $info, $l) = @_;
381    
382 root 1.2 64 == length $prk
383     or die "hkdf_sha512_expand: prk length mist be 64 octets";
384    
385 root 1.1 my $t = "";
386     my $okm = "";
387     my $i = 0;
388    
389     do {
390     $t = Digest::SHA::hmac_sha512 $t . $info . chr ++$i, $prk;
391     $okm .= $t;
392     } while $l > length $okm;
393    
394     substr $okm, 0, $l
395     }
396    
397 root 1.5 sub normalize_udiff {
398     my $diff = shift;
399     my @line = split /\n/, $diff, -1;
400     my $delta = 0;
401     my $hunks = 0;
402     my @max_offset;
403    
404     pop @line while @line and !length $line[-1];
405    
406     for (my $i = 0; $i < @line; ++$i) {
407     next unless $line[$i] =~ /^@@ /;
408    
409     $line[$i] =~ /^@@ -(\d+)(?:,\d+)?(?: \+\d+(?:,\d+)?)? @@(.*)$/
410     or die "normalize_udiff: bad hunk header '$line[$i]'\n";
411    
412     my ($old_start, $suffix) = ($1, $2);
413     my ($old_count, $new_count) = (0, 0);
414     my %old_line;
415     my $j = $i + 1;
416    
417     # GNU patch mishandles hunks with more prefix than suffix context.
418     # Make the first context line an identity change without losing it.
419     # see https://unix.stackexchange.com/questions/414542/does-patch-work-with-asymmetric-context
420     splice @line, $i + 1, 1, "-$1", "+$1"
421     if $line[$i + 1] =~ /^ (.*)\z/;
422    
423     for (; $j < @line; ++$j) {
424     last if $line[$j] =~ /^@@ /;
425    
426     my $tag = substr $line[$j], 0, 1;
427    
428     if ($tag eq " ") {
429     ++$old_count;
430     ++$new_count;
431     $old_line{substr $line[$j], 1} = 1;
432     } elsif ($tag eq "-") {
433     ++$old_count;
434     $old_line{substr $line[$j], 1} = 1;
435     } elsif ($tag eq "+") {
436     ++$new_count;
437     } elsif ($line[$j] eq "\\ No newline at end of file") {
438     # Marker belongs to the preceding body line and counts neither side.
439     } else {
440     die "normalize_udiff: bad hunk line '$line[$j]'\n";
441     }
442     }
443    
444     $old_count or $new_count
445     or die "normalize_udiff: empty hunk at old line $old_start\n";
446    
447     $old_count or !$old_start
448     or die "normalize_udiff: hunk " . ($hunks + 1)
449     . " needs old-side context for insertion after old line $old_start\n";
450    
451     my $new_start = $old_start + $delta;
452     ++$new_start unless $old_count;
453     --$new_start unless $new_count;
454    
455     $new_start >= 0
456     or die "normalize_udiff: invalid new line $new_start\n";
457    
458     $line[$i] = sprintf "@@ -%d,%d +%d,%d @@%s",
459     $old_start, $old_count, $new_start, $new_count, $suffix;
460    
461     $delta += $new_count - $old_count;
462     ++$hunks;
463     my $max_offset = keys %old_line;
464     --$max_offset
465     if $max_offset;
466     push @max_offset, $max_offset;
467     $i = $j - 1;
468     }
469    
470     $hunks
471     or die "normalize_udiff: no hunks\n";
472    
473     (join ("\n", @line, ""), \@max_offset)
474     }
475    
476     sub parse_patch_output {
477     my ($output) = @_;
478     my (@hunks, @unparsed);
479    
480     for (split /\n/, $output) {
481     if (/^Hunk #(\d+) (succeeded|FAILED|ignored) at (-?\d+)(?: with fuzz (\d+))?(?: \(offset (-?\d+) lines?\))?\.\z/) {
482     push @hunks, {
483     hunk => 0 + $1,
484     success => $2 eq "succeeded",
485     line => 0 + $3,
486     fuzz => 0 + $4,
487     offset => 0 + $5,
488     };
489     } elsif (!length
490     || /^Hmm\.\.\. +Looks like a unified diff to me\.\.\.\z/
491     || /^The text leading up to this was:\z/
492     || /^-+\z/ || /^\|/
493     || /^patching file / || /^checking file /
494     || /^Using Plan [AB]\.\.\.\z/ || /^done\z/) {
495     # GNU patch --verbose framing, not hunk results.
496     } else {
497     push @unparsed, $_;
498     }
499     }
500    
501     { hunks => \@hunks, unparsed => \@unparsed }
502     }
503    
504     sub check_patch_output {
505     my ($status, $output, $max_offset) = @_;
506     my $parsed = parse_patch_output $output;
507     my $hunks = $parsed->{hunks};
508    
509     die "patch failed (wait status $status):\n$output"
510     if $status;
511     die "patch: unrecognized output:\n" . (join "\n", @{$parsed->{unparsed}}) . "\n"
512     if @{$parsed->{unparsed}};
513     die "patch: expected " . @$max_offset . " hunks, got " . @$hunks . "\n"
514     unless @$hunks == @$max_offset && @$hunks;
515    
516     for my $i (0 .. $#$hunks) {
517     my $hunk = $hunks->[$i];
518    
519     die "patch: unexpected hunk number $hunk->{hunk}\n"
520     unless $hunk->{hunk} == $i + 1;
521     die "patch: hunk $hunk->{hunk} failed\n"
522     unless $hunk->{success};
523     die "patch: hunk $hunk->{hunk} used fuzz $hunk->{fuzz}\n"
524     if $hunk->{fuzz};
525     die "patch: hunk $hunk->{hunk} offset $hunk->{offset} exceeds $max_offset->[$i]\n"
526     if abs $hunk->{offset} > $max_offset->[$i];
527     }
528    
529     $hunks
530     }
531    
532     sub apply_udiff {
533     my ($input, $output, $diff) = @_;
534     my ($normalized, $max_offset) = normalize_udiff $diff;
535    
536     require AnyEvent::Util;
537     require File::Spec;
538     require File::Basename;
539     require Fcntl;
540    
541     $input = File::Spec->rel2abs ($input);
542     $output = File::Spec->rel2abs ($output);
543     my $directory = File::Basename::dirname ($output);
544    
545     sysopen my $fh, $output, Fcntl::O_WRONLY () | Fcntl::O_CREAT () | Fcntl::O_EXCL (), 0600
546     or die "$output: $!\n";
547     close $fh or do { my $error = $!; unlink $output; die "$output: $error\n" };
548    
549     my $cv = AE::cv ();
550     my $log;
551     my $process = eval {
552     AnyEvent::Util::run_cmd ([
553     "patch", "--verbose", "--batch", "--forward", "--binary", "--fuzz=0",
554     "--no-backup-if-mismatch", "--reject-file=-", "--output=$output", "--", $input,
555     ],
556     "<" => \$normalized,
557     ">" => \$log,
558     close_all => 1,
559     on_prepare => sub {
560     $ENV{LC_ALL} = "C";
561     chdir $directory or die "$directory: $!\n";
562     defined POSIX::dup2 (1, 2) or die "dup2: $!\n";
563     },
564     );
565     };
566    
567     if ($@) {
568     my $error = $@;
569     unlink $output;
570     die $error;
571     }
572    
573     $process->cb (sub {
574     my $hunks;
575     eval { $hunks = check_patch_output $_[0]->recv, $log, $max_offset; 1 } or do {
576     my $error = $@;
577     unlink $output;
578     $cv->croak ($error);
579     return;
580     };
581    
582     $cv->send ($hunks);
583     });
584    
585     $cv
586     }
587    
588 root 1.1 sub _clean_text($) {
589     my ($text) = @_;
590    
591     my ($prefix) = (join "\n", grep /\S/, split /\n/, $text)
592     =~ /\A([ \t]*)[^\n]*(?:\n\1[^\n]*)*\z/;
593    
594     $text =~ s/^\Q$prefix\E//mg;
595     $text =~ s/\A\s+//;
596     $text =~ s/\s+\z//;
597     $text
598     }
599    
600     package AnyEvent::MCP::Base;
601    
602     sub new {
603     my ($class, %kv) = @_;
604     bless \%kv, $class
605     }
606    
607     sub event {
608     my ($self, $event, @args) = @_;
609     ($self->{"on_$event"} or return)->($self, @args)
610     }
611    
612     =head1 SEE ALSO
613    
614     L<AnyEvent>, L<https://modelcontextprotocol.io/>
615    
616     =head1 AUTHOR
617    
618     The main author of this module is OpenAI ChatGPT.
619    
620     Additional guidance received by:
621    
622     Marc Lehmann <schmorp@schmorp.de>
623     http://home.schmorp.de/
624    
625     =cut
626    
627     1
628