ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP.pm
Revision: 1.6
Committed: Thu Oct 8 02:03:51 2026 UTC (5 weeks, 1 day ago) by root
Branch: MAIN
Changes since 1.5: +92 -39 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     =over 4
34    
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     =item $bytes = AnyEvent::MCP::random_bytes $length
134 root 1.2
135     Returns exactly C<$length> random bytes. Tries C</dev/urandom>, then an external
136     C<openssl rand> command. No additional random-number module is required.
137     Dies if neither source supplies the requested bytes successfully.
138     C<$length> must be a nonnegative integer; zero returns an empty string.
139    
140 root 1.6 =back
141    
142     =head2 PATCH UTILITIES
143    
144     Since applying patches to files is a common taks for MCP servers, the
145     following utility functions can be used to ease that job. Together, they
146     define a ocmmon patch format (unified diff or a simplified version of it,
147     with fewer requirements to make mto easier for the LLM to generate), helpers
148     to apply the patch and a helper to verify if the patch was applied
149     with sufficient precision.
150    
151     The workflow would be:
152    
153     =over
154    
155     =item 1. AnyEvent::MCP::normalize_udiff
156    
157     Retursn a unified diff for GNU patch, and meta data to verify its application.
158    
159     =item 2. AnyEvent::MCP::apply_udiff
160    
161     This should capture GNU patch output.
162 root 1.5
163 root 1.6 =back
164    
165     If you apply the patch yourself, you should capture patch otuput, parse it
166     via C<parse_patch_output> and check limits via C<check_patch_output>.
167 root 1.5
168     =head3 Input format
169    
170 root 1.6 The patch format accepted by normalize_udiff is an extension to the
171     (essentially undocumented) unified diff format.
172    
173 root 1.5 In a hunk header, only the old start C<-N> matters. It is the old-file line
174     of the first hunk body line, including context, not necessarily the first
175     changed line. Counts and the new-file position are ignored and recomputed.
176     Both ordinary headers and the abbreviated form are accepted:
177    
178     @@ -42,99 +123,99 @@
179     @@ -42 @@
180    
181     Optional text after the closing C<@@> is preserved. File headers and other
182     text before the first hunk are preserved; a diff must describe one file.
183    
184     Hunk body lines use the usual prefixes: a space for unchanged context,
185     C<-> for removed lines and C<+> for added lines. The
186     C<\ No newline at end of file> marker is preserved and counts neither side.
187     Trailing empty lines outside the final hunk are discarded.
188    
189     Pure insertion is permitted only at the beginning, using old start zero.
190     An insertion elsewhere needs old-side context. Empty hunks are rejected.
191     New positions include the cumulative line-count changes of preceding hunks.
192    
193     =head3 Applying the result
194    
195 root 1.6 The C<normalize_udiff> function does not apply the patch, check file
196     contents or guard against concurrent edits. The caller must supply those
197     checks, if required.
198 root 1.5
199     Context must match exactly: apply with fuzz zero. For each hunk,
200     C<$max_offset> permits relocation by at most the number of distinct old-side
201     lines minus one, or zero for a pure insertion. Repeated old-side lines do
202     not increase this allowance. The applier must enforce these limits; an
203     unrestricted search for matching context can select the wrong occurrence.
204    
205     For live Vim buffers, also require the expected changedtick to match.
206     File-based users need an equivalent check before replacing the file.
207    
208 root 1.6 To work around GNU patch's handling of asymmetric context, the
209     first context line of each hunk is rewritten as an identity
210     removal/addition. The line contents and old/new line counts are
211     unchanged. This is normalization for the patch applier, not a relaxation
212     of context matching.
213 root 1.5
214 root 1.6 =over
215    
216     =item ($diff, $max_offset) = AnyEvent::MCP::normalize_udiff $diff
217    
218     Normalizes a relaxed, single-file unified diff into ordinary unified diff
219     syntax. Returns the normalized C<$diff> and an array reference C<$max_offset>
220     containing the permitted whole-hunk relocation for each hunk, in order.
221     Dies on malformed hunks or when no hunks are found.
222    
223     Here is a suitable description for the diff format, e.g. in a tool
224     definition:
225    
226     Apply a unified diff to one regular file. Hunk counts are recomputed,
227     fuzz is disabled, whole-hunk relocation is limited per hunk by distinct
228     old-side lines. Pure insertion is allowed only at the beginning;
229     elsewhere include old-side context.
230    
231     =item $cv = AnyEvent::MCP::apply_udiff $input, $output, $diff
232    
233     Asynchronously applies the relaxed C<$diff> to the input file C<$input>,
234     writing a separate output file C<$output>. C<$diff> must contain bytes;
235     callers working with characters must encode them for the file themselves.
236    
237     Normalizes the diff, runs GNU patch using C<AnyEvent::Util::run_cmd>, and
238     validates its verbose output with C<check_patch_output>. The command uses
239     batch/forward mode, binary mode and fuzz zero. No shell is involved.
240    
241     C<$output> must not exist. It is reserved with mode 0600; patch runs in its
242     directory so incidental temporary files stay there. Input and output paths
243     are made absolute before starting the command. These must be trusted paths
244     in a directory controlled by the caller.
245    
246     Returns a condition variable. Its C<recv> returns the validated hunk array
247     reference, or throws on command/application failure. Setup failures throw
248     synchronously. Failed or partial output is removed. C<$input> is unchanged.
249    
250     The caller owns committing the successful output and removing it when no
251     longer needed. Freshness checks, metadata preservation, atomic replacement
252     and application-specific permissions also belong to the caller.
253    
254     For your reference, the exact options used are:
255    
256     --verbose --batch --forward --binary --fuzz=0
257     --no-backup-if-mismatch
258     --reject-file=- --output=... -- <inputfile>
259    
260     =item $parsed = AnyEvent::MCP::parse_patch_output $output
261 root 1.5
262     Parses C<LC_ALL=C> GNU patch C<--verbose> output without judging whether
263     the application is acceptable. C<$parsed> has two fields:
264    
265 root 1.6 =over
266 root 1.5
267     =item hunks => [$hunk...]
268    
269     Hunk records in output order. Each record contains C<hunk> (the one-based
270     number), C<success>, C<line> (the reported application line), C<fuzz> and
271     the signed C<offset>. Omitted fuzz and offset are zero. Failed and ignored
272     hunks have false C<success>.
273    
274     =item unparsed => [$line...]
275    
276     Lines not recognized as hunk results or GNU patch's verbose framing.
277     The parser preserves them for callers to interpret or reject.
278    
279     =back
280    
281     Use verbose output: ordinary patch output omits exact-position successes,
282     so it cannot account for every hunk.
283    
284 root 1.6 =item $hunks = AnyEvent::MCP::check_patch_output $status, $output, [$max_offset...]
285 root 1.5
286     Parses C<$output> and validates a complete application. C<$status> is the
287     raw process wait status, as returned by C<waitpid> or
288     C<AnyEvent::Util::run_cmd>, not just the exit code.
289    
290     Requires status zero, no unrecognized output, exactly the expected hunks
291     in numbered order, successful application, fuzz zero and each signed offset
292     within its corresponding absolute limit. C<[$max_offset...]> is the array
293     reference returned by C<normalize_udiff>. At least one hunk is required.
294    
295     Returns the parsed hunk array reference on success; otherwise dies with a
296     diagnostic. This is also suitable for callers running patch inside their
297     own sandbox.
298    
299 root 1.6 =back
300 root 1.5
301 root 1.1 =cut
302    
303     package AnyEvent::MCP;
304    
305     use common::sense;
306     use Digest::SHA ();
307    
308     our $VERSION = '0.0';
309    
310 root 1.2 sub random_bytes($) {
311     my ($length) = @_;
312    
313     die "Invalid random byte count\n"
314     unless defined $length && $length =~ /\A[0-9]+\z/;
315     return "" unless $length;
316    
317     my $bytes = "";
318    
319     if (open my $fh, "<", "/dev/urandom") {
320     binmode $fh;
321     while (length $bytes < $length) {
322     read ($fh, $bytes, $length - length ($bytes), length $bytes)
323     or last;
324     }
325     close $fh;
326    
327     return $bytes if length $bytes == $length;
328     }
329    
330     if (open my $fh, "-|", "openssl", "rand", $length) {
331     binmode $fh;
332     local $/;
333     $bytes = <$fh>;
334     my $success = close $fh;
335    
336     return $bytes
337     if $success && defined $bytes && length $bytes == $length;
338     }
339    
340     die "Cannot obtain $length random bytes from /dev/urandom or openssl rand\n";
341     }
342    
343 root 1.1 sub hkdf_sha512_expand($$$) {
344     my ($prk, $info, $l) = @_;
345    
346 root 1.2 64 == length $prk
347     or die "hkdf_sha512_expand: prk length mist be 64 octets";
348    
349 root 1.1 my $t = "";
350     my $okm = "";
351     my $i = 0;
352    
353     do {
354     $t = Digest::SHA::hmac_sha512 $t . $info . chr ++$i, $prk;
355     $okm .= $t;
356     } while $l > length $okm;
357    
358     substr $okm, 0, $l
359     }
360    
361 root 1.5 sub normalize_udiff {
362     my $diff = shift;
363     my @line = split /\n/, $diff, -1;
364     my $delta = 0;
365     my $hunks = 0;
366     my @max_offset;
367    
368     pop @line while @line and !length $line[-1];
369    
370     for (my $i = 0; $i < @line; ++$i) {
371     next unless $line[$i] =~ /^@@ /;
372    
373     $line[$i] =~ /^@@ -(\d+)(?:,\d+)?(?: \+\d+(?:,\d+)?)? @@(.*)$/
374     or die "normalize_udiff: bad hunk header '$line[$i]'\n";
375    
376     my ($old_start, $suffix) = ($1, $2);
377     my ($old_count, $new_count) = (0, 0);
378     my %old_line;
379     my $j = $i + 1;
380    
381     # GNU patch mishandles hunks with more prefix than suffix context.
382     # Make the first context line an identity change without losing it.
383     # see https://unix.stackexchange.com/questions/414542/does-patch-work-with-asymmetric-context
384     splice @line, $i + 1, 1, "-$1", "+$1"
385     if $line[$i + 1] =~ /^ (.*)\z/;
386    
387     for (; $j < @line; ++$j) {
388     last if $line[$j] =~ /^@@ /;
389    
390     my $tag = substr $line[$j], 0, 1;
391    
392     if ($tag eq " ") {
393     ++$old_count;
394     ++$new_count;
395     $old_line{substr $line[$j], 1} = 1;
396     } elsif ($tag eq "-") {
397     ++$old_count;
398     $old_line{substr $line[$j], 1} = 1;
399     } elsif ($tag eq "+") {
400     ++$new_count;
401     } elsif ($line[$j] eq "\\ No newline at end of file") {
402     # Marker belongs to the preceding body line and counts neither side.
403     } else {
404     die "normalize_udiff: bad hunk line '$line[$j]'\n";
405     }
406     }
407    
408     $old_count or $new_count
409     or die "normalize_udiff: empty hunk at old line $old_start\n";
410    
411     $old_count or !$old_start
412     or die "normalize_udiff: hunk " . ($hunks + 1)
413     . " needs old-side context for insertion after old line $old_start\n";
414    
415     my $new_start = $old_start + $delta;
416     ++$new_start unless $old_count;
417     --$new_start unless $new_count;
418    
419     $new_start >= 0
420     or die "normalize_udiff: invalid new line $new_start\n";
421    
422     $line[$i] = sprintf "@@ -%d,%d +%d,%d @@%s",
423     $old_start, $old_count, $new_start, $new_count, $suffix;
424    
425     $delta += $new_count - $old_count;
426     ++$hunks;
427     my $max_offset = keys %old_line;
428     --$max_offset
429     if $max_offset;
430     push @max_offset, $max_offset;
431     $i = $j - 1;
432     }
433    
434     $hunks
435     or die "normalize_udiff: no hunks\n";
436    
437     (join ("\n", @line, ""), \@max_offset)
438     }
439    
440     sub parse_patch_output {
441     my ($output) = @_;
442     my (@hunks, @unparsed);
443    
444     for (split /\n/, $output) {
445     if (/^Hunk #(\d+) (succeeded|FAILED|ignored) at (-?\d+)(?: with fuzz (\d+))?(?: \(offset (-?\d+) lines?\))?\.\z/) {
446     push @hunks, {
447     hunk => 0 + $1,
448     success => $2 eq "succeeded",
449     line => 0 + $3,
450     fuzz => 0 + $4,
451     offset => 0 + $5,
452     };
453     } elsif (!length
454     || /^Hmm\.\.\. +Looks like a unified diff to me\.\.\.\z/
455     || /^The text leading up to this was:\z/
456     || /^-+\z/ || /^\|/
457     || /^patching file / || /^checking file /
458     || /^Using Plan [AB]\.\.\.\z/ || /^done\z/) {
459     # GNU patch --verbose framing, not hunk results.
460     } else {
461     push @unparsed, $_;
462     }
463     }
464    
465     { hunks => \@hunks, unparsed => \@unparsed }
466     }
467    
468     sub check_patch_output {
469     my ($status, $output, $max_offset) = @_;
470     my $parsed = parse_patch_output $output;
471     my $hunks = $parsed->{hunks};
472    
473     die "patch failed (wait status $status):\n$output"
474     if $status;
475     die "patch: unrecognized output:\n" . (join "\n", @{$parsed->{unparsed}}) . "\n"
476     if @{$parsed->{unparsed}};
477     die "patch: expected " . @$max_offset . " hunks, got " . @$hunks . "\n"
478     unless @$hunks == @$max_offset && @$hunks;
479    
480     for my $i (0 .. $#$hunks) {
481     my $hunk = $hunks->[$i];
482    
483     die "patch: unexpected hunk number $hunk->{hunk}\n"
484     unless $hunk->{hunk} == $i + 1;
485     die "patch: hunk $hunk->{hunk} failed\n"
486     unless $hunk->{success};
487     die "patch: hunk $hunk->{hunk} used fuzz $hunk->{fuzz}\n"
488     if $hunk->{fuzz};
489     die "patch: hunk $hunk->{hunk} offset $hunk->{offset} exceeds $max_offset->[$i]\n"
490     if abs $hunk->{offset} > $max_offset->[$i];
491     }
492    
493     $hunks
494     }
495    
496     sub apply_udiff {
497     my ($input, $output, $diff) = @_;
498     my ($normalized, $max_offset) = normalize_udiff $diff;
499    
500     require AnyEvent::Util;
501     require File::Spec;
502     require File::Basename;
503     require Fcntl;
504    
505     $input = File::Spec->rel2abs ($input);
506     $output = File::Spec->rel2abs ($output);
507     my $directory = File::Basename::dirname ($output);
508    
509     sysopen my $fh, $output, Fcntl::O_WRONLY () | Fcntl::O_CREAT () | Fcntl::O_EXCL (), 0600
510     or die "$output: $!\n";
511     close $fh or do { my $error = $!; unlink $output; die "$output: $error\n" };
512    
513     my $cv = AE::cv ();
514     my $log;
515     my $process = eval {
516     AnyEvent::Util::run_cmd ([
517     "patch", "--verbose", "--batch", "--forward", "--binary", "--fuzz=0",
518     "--no-backup-if-mismatch", "--reject-file=-", "--output=$output", "--", $input,
519     ],
520     "<" => \$normalized,
521     ">" => \$log,
522     close_all => 1,
523     on_prepare => sub {
524     $ENV{LC_ALL} = "C";
525     chdir $directory or die "$directory: $!\n";
526     defined POSIX::dup2 (1, 2) or die "dup2: $!\n";
527     },
528     );
529     };
530    
531     if ($@) {
532     my $error = $@;
533     unlink $output;
534     die $error;
535     }
536    
537     $process->cb (sub {
538     my $hunks;
539     eval { $hunks = check_patch_output $_[0]->recv, $log, $max_offset; 1 } or do {
540     my $error = $@;
541     unlink $output;
542     $cv->croak ($error);
543     return;
544     };
545    
546     $cv->send ($hunks);
547     });
548    
549     $cv
550     }
551    
552 root 1.1 sub _clean_text($) {
553     my ($text) = @_;
554    
555     my ($prefix) = (join "\n", grep /\S/, split /\n/, $text)
556     =~ /\A([ \t]*)[^\n]*(?:\n\1[^\n]*)*\z/;
557    
558     $text =~ s/^\Q$prefix\E//mg;
559     $text =~ s/\A\s+//;
560     $text =~ s/\s+\z//;
561     $text
562     }
563    
564     package AnyEvent::MCP::Base;
565    
566     sub new {
567     my ($class, %kv) = @_;
568     bless \%kv, $class
569     }
570    
571     sub event {
572     my ($self, $event, @args) = @_;
573     ($self->{"on_$event"} or return)->($self, @args)
574     }
575    
576     =head1 SEE ALSO
577    
578     L<AnyEvent>, L<https://modelcontextprotocol.io/>
579    
580     =head1 AUTHOR
581    
582     The main author of this module is OpenAI ChatGPT.
583    
584     Additional guidance received by:
585    
586     Marc Lehmann <schmorp@schmorp.de>
587     http://home.schmorp.de/
588    
589     =cut
590    
591     1
592