ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP.pm
Revision: 1.9
Committed: Fri Oct 9 07:59:23 2026 UTC (5 weeks, 1 day ago) by root
Branch: MAIN
Changes since 1.8: +270 -2 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.9 The interfaces are still being designed. The walkthrough below explains the
64     installed programs; module-specific POD documents their Perl APIs.
65    
66     =head1 GETTING STARTED
67    
68     This distribution contains an MCP client/server library and several ready-to-use
69     programs. You do not need to write Perl to use them.
70    
71     =over
72    
73     =item C<mcp-fs>
74    
75     A filesystem and command server. Each operation runs in a Linux bubblewrap
76     sandbox with only the filesystem and other capabilities approved for that
77     operation. Read, write, search, patch, path operations and command execution
78     are exposed as MCP tools.
79    
80     =item C<mcp-multibroker>
81    
82     Combines several MCP servers behind one connection. Each downstream server has
83     a prefix, so a C<file-read> tool belonging to C<files> becomes
84     C<files.file-read>. Its clients may connect over HTTP or be launched over
85     stdio. The multibroker itself can serve HTTP or stdio.
86    
87     =item C<anyevent-mcp> and C<anyevent-mcp-askpermission>
88    
89     Manage shared credentials/rules and ask the local user for permission,
90     respectively. Normally the permission helper is started automatically.
91    
92     =back
93    
94     =head2 Start with one filesystem server
95    
96     Install this Perl distribution first. On Debian or Ubuntu, install the
97     runtime sandbox and optional graphical permission helper with:
98    
99     sudo apt install bubblewrap yad
100    
101     The executable is called C<bwrap>, although the package is C<bubblewrap>.
102     C<yad> provides graphical approval dialogs; without it, the default helper
103     tries C</dev/tty> and denies if no terminal is available. Unprivileged user
104     namespaces must be available. C<mcp-fs> also uses GNU C<find>, C<grep>,
105     C<patch> and C<sed>, and a compatible Perl interpreter. On a minimal
106     Debian installation, these can be installed with:
107    
108     sudo apt install findutils grep patch sed perl
109    
110     Tools whose required programs are missing are omitted at startup.
111    
112     Give the server one or more directories that you actually intend to make
113     available. For example:
114    
115     mcp-fs --name code --port 31741 /home/alice/src/project
116    
117     This starts an authenticated HTTP MCP server. It prints a URL containing
118     an access cookie; keep that URL private. Its clean URL also accepts the same
119     credential as a bearer token. C<--port 0> selects an available port.
120     The permission-rule file is printed on stderr.
121    
122     For a local MCP client that starts its servers as subprocesses, use stdio:
123    
124     mcp-fs --stdio /home/alice/src/project
125    
126     The MCP client, not a human terminal, should start this process: stdin and
127     stdout carry protocol messages. For a quick manual sandbox test without MCP
128     or permission prompting, run from the intended workdir:
129    
130     mcp-fs --exec-cmd workdir-read -- ls
131    
132     C<--exec-cmd> runs the supplied command with exactly the named capabilities,
133     without consulting saved permission rules. It is a local debugging facility.
134    
135     Within MCP, begin with the C<workdirs> tool to inspect the configured
136     workdirs and capabilities. Relative paths are resolved against the selected
137     workdir; access beyond the granted mounts needs additional permission.
138     Providing a workdir does not give arbitrary commands unrestricted host access.
139     See L<mcp-fs/CAPABILITIES> and L<mcp-fs/LIMITATIONS>.
140    
141     Once the client is connected, you can just ask the LLM to use the tools,
142     for example:
143    
144     Show me the available workdirs, find the TODO comments in my project,
145     and suggest the next task. Do not modify any files.
146    
147     The client can call C<workdirs> and C<grep> itself; you need not know the
148     MCP request format or write your own integration.
149    
150     =head2 Combine servers with mcp-multibroker
151    
152     Rather than configure each MCP client with every server, configure them
153     once behind the multibroker. For example, to expose two distinct filesystem
154     servers over one HTTP endpoint:
155    
156     mcp-multibroker code 'stdio:mcp-fs /home/alice/src/project' \
157     notes 'stdio:mcp-fs /home/alice/notes'
158    
159     Each C<stdio:> value is a shell command; quote it as one argument.
160     The multibroker starts the child with the correct stdio environment
161     and an alias equal to its prefix. That alias is also the default
162     C<mcp-fs> permission profile, keeping permissions for C<code> and
163     C<notes> separate.
164    
165     The multibroker prints its own authenticated HTTP URL. A client sees
166     tools such as C<code.file-read>, C<code.file-patch> and
167     C<notes.file-read>. The additional C<discover-brokers>, C<list-tools>
168     and C<call-tool> tools support explicit discovery and dispatch.
169     The client normally can call the prefixed tools directly.
170    
171     If the LLM is still using an old tool list after a downstream server
172     becomes available or changes its tools, simply tell it:
173    
174     Call discover-brokers and list-tools again, then use the newly
175     discovered tools.
176    
177     C<discover-brokers> connects to reachable servers and retrieves their
178     instructions; C<list-tools> requests their current tool lists. C<call-tool>
179     can dispatch a newly discovered prefixed tool even when the client has not
180     refreshed its own direct tool list yet. If the client hides or caches the
181     broker's tools, reconnect or refresh the MCP connection in that client.
182     Changing the broker's configured prefix/URL pairs requires restarting
183     C<mcp-multibroker>; discovery cannot change its command-line configuration.
184    
185     The same broker can mix remote HTTP servers with local subprocesses.
186     For example, an installation might combine office and automation tools:
187    
188     mcp-multibroker \
189     office http://localhost:8766/mcp \
190     homeassistant http://homeassistant.local:8123/api/mcp \
191     --bearer TOKEN \
192     marco http://marco.example.net:31746/COOKIE/
193    
194     Or combine a custom application server, remote machines and a third-party
195     stdio server:
196    
197     mcp-multibroker \
198     agni 'stdio:/opt/agni/bin/agni-mcp' \
199     cerebro http://cerebro.example.net:31748/COOKIE/ \
200     chrome 'stdio:npx -y chrome-devtools-mcp@latest --no-usage-statistics'
201    
202     These are illustrative hostnames and redacted credentials, not usable
203     endpoints. C<--bearer> belongs immediately after the HTTP server it
204     authenticates. A cookie URL already carries its credential. Never publish
205     or check in either kind of secret. The broker does not impose the C<mcp-fs>
206     bubblewrap sandbox on other servers: those processes and services have their
207     own security boundaries. A shell wrapper can use C<exec mcp-multibroker>
208     to replace itself with the broker process.
209    
210     To start the multibroker itself over stdio, let the client set
211     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio>, or use a command like:
212    
213     PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio \
214     mcp-multibroker code 'stdio:mcp-fs /home/alice/src/project'
215    
216     The broker can also connect to existing HTTP MCP servers:
217    
218     mcp-multibroker files 'stdio:mcp-fs /home/alice/src/project' \
219     remote https://example.org/mcp/ --bearer TOKEN
220    
221     The token is a credential; avoid exposing it in shell history or process
222     arguments in a real deployment. An HTTP client can instead use
223     C<[client PREFIX]> in the private global configuration. See
224     L<mcp-multibroker> and L</GLOBAL CONFIGURATION>.
225    
226     =head2 Connect an AI client
227    
228     These are examples for clients with MCP support. Paths are illustrative;
229     substitute your installed commands and real workdirs. Client interfaces
230     evolve independently of this module, so consult their documentation if an
231     option has changed.
232    
233     =head3 OpenAI Codex CLI
234    
235     Codex can start the broker locally over stdio:
236    
237     codex mcp add mytools --env PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio \
238     -- mcp-multibroker \
239     files 'stdio:mcp-fs /home/alice/src/project'
240     codex mcp list
241    
242     Use C</mcp> within Codex to inspect connections.
243     See L<Codex MCP documentation|https://learn.chatgpt.com/docs/extend/mcp>.
244    
245     =head3 Anthropic Claude Code
246    
247     Claude Code can start the same broker:
248    
249     claude mcp add --transport stdio --scope user mytools \
250     --env PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio -- \
251     mcp-multibroker files 'stdio:mcp-fs /home/alice/src/project'
252     claude mcp list
253    
254     Use C</mcp> within Claude Code for connection status. See
255     L<Claude Code MCP documentation|https://code.claude.com/docs/en/mcp>.
256    
257     =head3 Google Gemini CLI
258    
259     Add a local broker to C<~/.gemini/settings.json> (merge with any existing
260     settings):
261    
262     {
263     "mcpServers": {
264     "mytools": {
265     "command": "mcp-multibroker",
266     "args": [
267     "files", "stdio:mcp-fs /home/alice/src/project"
268     ],
269     "env": {
270     "PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL": "stdio"
271     }
272     }
273     }
274     }
275    
276     Use C</mcp> in Gemini CLI to inspect the connection.
277     See L<Gemini CLI MCP documentation|https://geminicli.com/docs/tools/mcp-server/>.
278    
279     =head3 ChatGPT web
280    
281     ChatGPT is a hosted client and cannot spawn a local stdio process or
282     connect directly to a local loopback server. Start C<mcp-multibroker> in
283     HTTP mode and connect it through ChatGPT's custom MCP plugin setup.
284     For a private local server, use an appropriately configured Secure MCP
285     Tunnel rather than making the machine publicly accessible. The connection
286     is subject to the client's plan, workspace and feature permissions.
287     See L<ChatGPT custom MCP servers|https://developers.openai.com/api/docs/guides/custom-mcp-server>
288     and L<Secure MCP Tunnel|https://developers.openai.com/blog/connect-private-mcp-servers-to-openai-products>.
289    
290     =head2 Permissions and approval dialogs
291    
292     C<mcp-fs> does not treat a connected model as fully trusted. Each tool
293     asks for the authority it needs; you can allow or deny the request once,
294     or save a rule for subsequent matching requests. Its default prompting
295     mode is C<--ask helper>, using C<anyevent-mcp-askpermission> on the
296     server's machine (YAD or C</dev/tty>). Closing the prompt denies once.
297    
298     For a client that supports the newer MCP form-elicitation protocol, try:
299    
300     mcp-fs --ask auto --stdio /home/alice/src/project
301    
302     C<--ask auto> uses MCP elicitation when the client advertises the necessary
303     support, otherwise the local helper. C<--ask mcp> requires elicitation and
304     fails if it is unavailable; it does not silently fall back. Not all MCP
305     clients or versions support the required input/continuation flow, even if
306     they support ordinary tool calls. For an ordinary stdio broker configuration,
307     put the chosen option in the child command:
308    
309     mcp-multibroker files \
310     'stdio:mcp-fs --ask auto /home/alice/src/project'
311    
312     Permission profiles are stored under C<~/.config/anyevent-mcp/> (or
313     C<$XDG_CONFIG_HOME/anyevent-mcp/>). A child launched by the broker
314     normally uses its prefix as the profile name; use C<--profile NAME>
315     to override it. Review persistent rules rather than granting
316     C<world-write> casually. Run C<anyevent-mcp --help> for the credential
317     and rule-management commands; see
318     L<AnyEvent::MCP::Server::Permissions> for rule syntax and ordering.
319    
320     =head2 Further documentation
321    
322     perldoc mcp-fs
323     perldoc mcp-multibroker
324     perldoc anyevent-mcp
325     perldoc AnyEvent::MCP::Server::Simple
326     perldoc AnyEvent::MCP::Server::Permissions
327    
328     The programs describe their own command-line options. If a client reports
329     no tools, first check that the selected program starts and that its required
330     utilities are detected. Stdio servers must not print diagnostics or banners
331     to stdout. For HTTP, check that the client can reach the configured URL and
332     has the correct authentication credentials.
333 root 1.1
334 root 1.2 =head1 GLOBAL CONFIGURATION
335    
336     The shared per-user configuration is C<$XDG_CONFIG_HOME/anyevent-mcp/global.ini>,
337     or C<$HOME/.config/anyevent-mcp/global.ini> when XDG_CONFIG_HOME is unset.
338     L<AnyEvent::MCP::Config> loads and holds it; loading this module does not access
339     configuration files.
340    
341     The file is UTF-8 and uses sections with single-line C<key = value> pairs.
342     Values are unquoted strings. Blank lines and whole-line comments beginning
343     with C<#> or C<;> are ignored; inline comment characters remain part of the
344     value. Later occurrences of a key replace earlier ones in the same section.
345    
346     [global]
347     secret = ...128 hexadecimal digits...
348    
349     [server example]
350 root 1.3 port = 31740
351     url = https://example.net:31740/{cookie}/
352 root 1.2
353     [mcp-fs]
354     utility = /path/to/utility
355    
356     Section names are unrestricted. Application-specific sections, including
357     exports, utility paths and user settings, are documented by their applications.
358    
359     C<[global]> holds C<secret>, a 64-byte master key encoded as 128 hexadecimal
360     digits. The file must be owned by the current user, with no group or other
361     permissions. A missing secret is generated automatically on first configuration
362     access and saved when possible. Existing secrets are never replaced implicitly.
363     An invalid secret is an error.
364    
365     If saving fails, Config warns and retains the secret in memory. Credentials
366     then remain stable only for that process; other processes cannot derive the
367     same credentials. If randomness is unavailable, supply a securely generated
368     secret in the file.
369    
370     C<[server NAME]> holds named-server settings. Its C<url> is a complete template;
371     C<{cookie}> is replaced with the cookie derived for that server and optional
372     username. Scheme, host, port and cookie placement are explicit in the template.
373 root 1.3 HTTP servers with an explicit C<name> load C<host>, C<port>, C<path>,
374     C<url_host>, C<url_scheme>, C<cookie>, C<bearer> and C<users> from this section
375     as defaults. C<users> is a comma-separated allowlist whose Basic passwords
376     are derived for the named server. An explicit password hash can instead be
377     passed to the constructor. C<url> is a template for the utility program; it
378     does not set the listener's address.
379    
380     C<mcp_start> chooses an application name and supplies cookie/bearer
381     authentication by default. Explicit authentication settings replace this
382     default. Stdio startup does not access global configuration.
383    
384     C<[client NAME]> supplies HTTP client defaults: C<url>, C<bearer>, C<username>,
385     C<password> and C<timeout>. Clients consult it only when explicitly given a
386     C<name>. An unnamed client or HTTP server does not read global configuration.
387     Explicit constructor arguments take precedence over named defaults.
388    
389     =head1 ENVIRONMENT
390    
391     Project-specific environment variables use the C<PERL_ANYEVENT_MCP_> prefix.
392     C<PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL=stdio> selects stdio in Simple.
393     C<PERL_ANYEVENT_MCP_MULTIBROKER_ALIAS> supplies the default application name.
394     C<PERL_ANYEVENT_MCP_PERMISSION_HELPER> selects the external asking executable.
395     Standard C<HOME> and C<XDG_CONFIG_HOME> keep their standard names.
396 root 1.2
397     =head1 FUNCTIONS
398    
399 root 1.6 =over
400    
401 root 1.8 =item $bytes = AnyEvent::MCP::utf8enc $text
402    
403     Return a UTF-8 byte string for C<$text>, leaving the argument unchanged.
404    
405     =item $text = AnyEvent::MCP::utf8dec $bytes
406    
407     Decode a UTF-8 byte string into characters, dying on invalid UTF-8.
408     The argument is left unchanged. For intentional in-place conversions,
409     use Perl's C<utf8::encode> or C<utf8::decode> directly.
410    
411 root 1.6 =item $bytes = AnyEvent::MCP::random_bytes $length
412 root 1.2
413     Returns exactly C<$length> random bytes. Tries C</dev/urandom>, then an external
414     C<openssl rand> command. No additional random-number module is required.
415     Dies if neither source supplies the requested bytes successfully.
416     C<$length> must be a nonnegative integer; zero returns an empty string.
417    
418 root 1.6 =back
419    
420     =head2 PATCH UTILITIES
421    
422 root 1.7 Since applying patches to files is a common task for MCP servers, the
423 root 1.6 following utility functions can be used to ease that job. Together, they
424 root 1.7 define a common patch format (unified diff or a simplified version of it,
425     with fewer requirements to make it easier for the LLM to generate), helpers
426 root 1.6 to apply the patch and a helper to verify if the patch was applied
427     with sufficient precision.
428    
429 root 1.7 Use C<apply_udiff> to normalize, apply and validate a patch in one operation.
430     It writes a separate output file; the caller checks freshness and metadata
431     and commits the result.
432    
433     For an application-specific applier, the workflow is:
434 root 1.6
435     =over
436    
437     =item 1. AnyEvent::MCP::normalize_udiff
438    
439 root 1.7 Returns a unified diff for GNU patch and the permitted hunk offsets.
440    
441     =item 2. Apply the normalized diff
442 root 1.6
443 root 1.7 Run GNU patch with fuzz zero and capture its verbose output.
444 root 1.6
445 root 1.7 =item 3. AnyEvent::MCP::check_patch_output
446    
447     Checks the process status and output against the permitted offsets.
448     C<parse_patch_output> is available separately when only parsing is needed.
449 root 1.5
450 root 1.6 =back
451    
452 root 1.5 =head3 Input format
453    
454 root 1.6 The patch format accepted by normalize_udiff is an extension to the
455     (essentially undocumented) unified diff format.
456    
457 root 1.5 In a hunk header, only the old start C<-N> matters. It is the old-file line
458     of the first hunk body line, including context, not necessarily the first
459     changed line. Counts and the new-file position are ignored and recomputed.
460     Both ordinary headers and the abbreviated form are accepted:
461    
462     @@ -42,99 +123,99 @@
463     @@ -42 @@
464    
465     Optional text after the closing C<@@> is preserved. File headers and other
466     text before the first hunk are preserved; a diff must describe one file.
467    
468     Hunk body lines use the usual prefixes: a space for unchanged context,
469     C<-> for removed lines and C<+> for added lines. The
470     C<\ No newline at end of file> marker is preserved and counts neither side.
471     Trailing empty lines outside the final hunk are discarded.
472    
473 root 1.7 Pure insertion is permitted only at the beginning, using old start C<0>.
474     For example, C<@@ -0 @@> with one added line becomes C<@@ -0,0 +1,1 @@>.
475     An empty old-side range names the line before the insertion: C<-0,0>
476     means before line 1, whereas C<-1,0> would mean after line 1 and is rejected
477     here. Nonempty ranges use one-based line numbers.
478    
479 root 1.5 An insertion elsewhere needs old-side context. Empty hunks are rejected.
480     New positions include the cumulative line-count changes of preceding hunks.
481    
482     =head3 Applying the result
483    
484 root 1.6 The C<normalize_udiff> function does not apply the patch, check file
485     contents or guard against concurrent edits. The caller must supply those
486     checks, if required.
487 root 1.5
488     Context must match exactly: apply with fuzz zero. For each hunk,
489     C<$max_offset> permits relocation by at most the number of distinct old-side
490     lines minus one, or zero for a pure insertion. Repeated old-side lines do
491     not increase this allowance. The applier must enforce these limits; an
492     unrestricted search for matching context can select the wrong occurrence.
493    
494     For live Vim buffers, also require the expected changedtick to match.
495     File-based users need an equivalent check before replacing the file.
496    
497 root 1.6 To work around GNU patch's handling of asymmetric context, the
498     first context line of each hunk is rewritten as an identity
499     removal/addition. The line contents and old/new line counts are
500     unchanged. This is normalization for the patch applier, not a relaxation
501     of context matching.
502 root 1.5
503 root 1.6 =over
504    
505     =item ($diff, $max_offset) = AnyEvent::MCP::normalize_udiff $diff
506    
507     Normalizes a relaxed, single-file unified diff into ordinary unified diff
508     syntax. Returns the normalized C<$diff> and an array reference C<$max_offset>
509     containing the permitted whole-hunk relocation for each hunk, in order.
510     Dies on malformed hunks or when no hunks are found.
511    
512     Here is a suitable description for the diff format, e.g. in a tool
513     definition:
514    
515     Apply a unified diff to one regular file. Hunk counts are recomputed,
516     fuzz is disabled, whole-hunk relocation is limited per hunk by distinct
517     old-side lines. Pure insertion is allowed only at the beginning;
518     elsewhere include old-side context.
519    
520     =item $cv = AnyEvent::MCP::apply_udiff $input, $output, $diff
521    
522     Asynchronously applies the relaxed C<$diff> to the input file C<$input>,
523     writing a separate output file C<$output>. C<$diff> must contain bytes;
524     callers working with characters must encode them for the file themselves.
525    
526     Normalizes the diff, runs GNU patch using C<AnyEvent::Util::run_cmd>, and
527     validates its verbose output with C<check_patch_output>. The command uses
528     batch/forward mode, binary mode and fuzz zero. No shell is involved.
529    
530     C<$output> must not exist. It is reserved with mode 0600; patch runs in its
531     directory so incidental temporary files stay there. Input and output paths
532     are made absolute before starting the command. These must be trusted paths
533     in a directory controlled by the caller.
534    
535     Returns a condition variable. Its C<recv> returns the validated hunk array
536     reference, or throws on command/application failure. Setup failures throw
537     synchronously. Failed or partial output is removed. C<$input> is unchanged.
538    
539     The caller owns committing the successful output and removing it when no
540     longer needed. Freshness checks, metadata preservation, atomic replacement
541     and application-specific permissions also belong to the caller.
542    
543     For your reference, the exact options used are:
544    
545     --verbose --batch --forward --binary --fuzz=0
546     --no-backup-if-mismatch
547     --reject-file=- --output=... -- <inputfile>
548    
549     =item $parsed = AnyEvent::MCP::parse_patch_output $output
550 root 1.5
551     Parses C<LC_ALL=C> GNU patch C<--verbose> output without judging whether
552     the application is acceptable. C<$parsed> has two fields:
553    
554 root 1.6 =over
555 root 1.5
556     =item hunks => [$hunk...]
557    
558     Hunk records in output order. Each record contains C<hunk> (the one-based
559     number), C<success>, C<line> (the reported application line), C<fuzz> and
560     the signed C<offset>. Omitted fuzz and offset are zero. Failed and ignored
561     hunks have false C<success>.
562    
563     =item unparsed => [$line...]
564    
565     Lines not recognized as hunk results or GNU patch's verbose framing.
566     The parser preserves them for callers to interpret or reject.
567    
568     =back
569    
570     Use verbose output: ordinary patch output omits exact-position successes,
571     so it cannot account for every hunk.
572    
573 root 1.6 =item $hunks = AnyEvent::MCP::check_patch_output $status, $output, [$max_offset...]
574 root 1.5
575     Parses C<$output> and validates a complete application. C<$status> is the
576     raw process wait status, as returned by C<waitpid> or
577     C<AnyEvent::Util::run_cmd>, not just the exit code.
578    
579     Requires status zero, no unrecognized output, exactly the expected hunks
580     in numbered order, successful application, fuzz zero and each signed offset
581     within its corresponding absolute limit. C<[$max_offset...]> is the array
582     reference returned by C<normalize_udiff>. At least one hunk is required.
583    
584     Returns the parsed hunk array reference on success; otherwise dies with a
585     diagnostic. This is also suitable for callers running patch inside their
586     own sandbox.
587    
588 root 1.6 =back
589 root 1.5
590 root 1.1 =cut
591    
592     package AnyEvent::MCP;
593    
594     use common::sense;
595     use Digest::SHA ();
596    
597     our $VERSION = '0.0';
598    
599 root 1.8 sub utf8enc($) {
600     my $text = shift;
601    
602     utf8::encode $text;
603     $text
604     }
605    
606     sub utf8dec($) {
607     my $bytes = shift;
608    
609     utf8::decode $bytes
610     or die "invalid UTF-8\n";
611     $bytes
612     }
613    
614 root 1.2 sub random_bytes($) {
615     my ($length) = @_;
616    
617     die "Invalid random byte count\n"
618     unless defined $length && $length =~ /\A[0-9]+\z/;
619     return "" unless $length;
620    
621     my $bytes = "";
622    
623     if (open my $fh, "<", "/dev/urandom") {
624     binmode $fh;
625     while (length $bytes < $length) {
626     read ($fh, $bytes, $length - length ($bytes), length $bytes)
627     or last;
628     }
629     close $fh;
630    
631     return $bytes if length $bytes == $length;
632     }
633    
634     if (open my $fh, "-|", "openssl", "rand", $length) {
635     binmode $fh;
636     local $/;
637     $bytes = <$fh>;
638     my $success = close $fh;
639    
640     return $bytes
641     if $success && defined $bytes && length $bytes == $length;
642     }
643    
644     die "Cannot obtain $length random bytes from /dev/urandom or openssl rand\n";
645     }
646    
647 root 1.1 sub hkdf_sha512_expand($$$) {
648     my ($prk, $info, $l) = @_;
649    
650 root 1.2 64 == length $prk
651     or die "hkdf_sha512_expand: prk length mist be 64 octets";
652    
653 root 1.1 my $t = "";
654     my $okm = "";
655     my $i = 0;
656    
657     do {
658     $t = Digest::SHA::hmac_sha512 $t . $info . chr ++$i, $prk;
659     $okm .= $t;
660     } while $l > length $okm;
661    
662     substr $okm, 0, $l
663     }
664    
665 root 1.5 sub normalize_udiff {
666     my $diff = shift;
667     my @line = split /\n/, $diff, -1;
668     my $delta = 0;
669     my $hunks = 0;
670     my @max_offset;
671    
672     pop @line while @line and !length $line[-1];
673    
674     for (my $i = 0; $i < @line; ++$i) {
675     next unless $line[$i] =~ /^@@ /;
676    
677     $line[$i] =~ /^@@ -(\d+)(?:,\d+)?(?: \+\d+(?:,\d+)?)? @@(.*)$/
678     or die "normalize_udiff: bad hunk header '$line[$i]'\n";
679    
680     my ($old_start, $suffix) = ($1, $2);
681     my ($old_count, $new_count) = (0, 0);
682     my %old_line;
683     my $j = $i + 1;
684    
685     # GNU patch mishandles hunks with more prefix than suffix context.
686     # Make the first context line an identity change without losing it.
687     # see https://unix.stackexchange.com/questions/414542/does-patch-work-with-asymmetric-context
688     splice @line, $i + 1, 1, "-$1", "+$1"
689     if $line[$i + 1] =~ /^ (.*)\z/;
690    
691     for (; $j < @line; ++$j) {
692     last if $line[$j] =~ /^@@ /;
693    
694     my $tag = substr $line[$j], 0, 1;
695    
696     if ($tag eq " ") {
697     ++$old_count;
698     ++$new_count;
699     $old_line{substr $line[$j], 1} = 1;
700     } elsif ($tag eq "-") {
701     ++$old_count;
702     $old_line{substr $line[$j], 1} = 1;
703     } elsif ($tag eq "+") {
704     ++$new_count;
705     } elsif ($line[$j] eq "\\ No newline at end of file") {
706     # Marker belongs to the preceding body line and counts neither side.
707     } else {
708     die "normalize_udiff: bad hunk line '$line[$j]'\n";
709     }
710     }
711    
712     $old_count or $new_count
713     or die "normalize_udiff: empty hunk at old line $old_start\n";
714    
715     $old_count or !$old_start
716     or die "normalize_udiff: hunk " . ($hunks + 1)
717     . " needs old-side context for insertion after old line $old_start\n";
718    
719     my $new_start = $old_start + $delta;
720     ++$new_start unless $old_count;
721     --$new_start unless $new_count;
722    
723     $new_start >= 0
724     or die "normalize_udiff: invalid new line $new_start\n";
725    
726     $line[$i] = sprintf "@@ -%d,%d +%d,%d @@%s",
727     $old_start, $old_count, $new_start, $new_count, $suffix;
728    
729     $delta += $new_count - $old_count;
730     ++$hunks;
731     my $max_offset = keys %old_line;
732     --$max_offset
733     if $max_offset;
734     push @max_offset, $max_offset;
735     $i = $j - 1;
736     }
737    
738     $hunks
739     or die "normalize_udiff: no hunks\n";
740    
741     (join ("\n", @line, ""), \@max_offset)
742     }
743    
744     sub parse_patch_output {
745     my ($output) = @_;
746     my (@hunks, @unparsed);
747    
748     for (split /\n/, $output) {
749     if (/^Hunk #(\d+) (succeeded|FAILED|ignored) at (-?\d+)(?: with fuzz (\d+))?(?: \(offset (-?\d+) lines?\))?\.\z/) {
750     push @hunks, {
751     hunk => 0 + $1,
752     success => $2 eq "succeeded",
753     line => 0 + $3,
754     fuzz => 0 + $4,
755     offset => 0 + $5,
756     };
757     } elsif (!length
758     || /^Hmm\.\.\. +Looks like a unified diff to me\.\.\.\z/
759     || /^The text leading up to this was:\z/
760     || /^-+\z/ || /^\|/
761     || /^patching file / || /^checking file /
762     || /^Using Plan [AB]\.\.\.\z/ || /^done\z/) {
763     # GNU patch --verbose framing, not hunk results.
764     } else {
765     push @unparsed, $_;
766     }
767     }
768    
769     { hunks => \@hunks, unparsed => \@unparsed }
770     }
771    
772     sub check_patch_output {
773     my ($status, $output, $max_offset) = @_;
774     my $parsed = parse_patch_output $output;
775     my $hunks = $parsed->{hunks};
776    
777     die "patch failed (wait status $status):\n$output"
778     if $status;
779     die "patch: unrecognized output:\n" . (join "\n", @{$parsed->{unparsed}}) . "\n"
780     if @{$parsed->{unparsed}};
781     die "patch: expected " . @$max_offset . " hunks, got " . @$hunks . "\n"
782     unless @$hunks == @$max_offset && @$hunks;
783    
784     for my $i (0 .. $#$hunks) {
785     my $hunk = $hunks->[$i];
786    
787     die "patch: unexpected hunk number $hunk->{hunk}\n"
788     unless $hunk->{hunk} == $i + 1;
789     die "patch: hunk $hunk->{hunk} failed\n"
790     unless $hunk->{success};
791     die "patch: hunk $hunk->{hunk} used fuzz $hunk->{fuzz}\n"
792     if $hunk->{fuzz};
793     die "patch: hunk $hunk->{hunk} offset $hunk->{offset} exceeds $max_offset->[$i]\n"
794     if abs $hunk->{offset} > $max_offset->[$i];
795     }
796    
797     $hunks
798     }
799    
800     sub apply_udiff {
801     my ($input, $output, $diff) = @_;
802     my ($normalized, $max_offset) = normalize_udiff $diff;
803    
804     require AnyEvent::Util;
805     require File::Spec;
806     require File::Basename;
807     require Fcntl;
808    
809     $input = File::Spec->rel2abs ($input);
810     $output = File::Spec->rel2abs ($output);
811     my $directory = File::Basename::dirname ($output);
812    
813     sysopen my $fh, $output, Fcntl::O_WRONLY () | Fcntl::O_CREAT () | Fcntl::O_EXCL (), 0600
814     or die "$output: $!\n";
815     close $fh or do { my $error = $!; unlink $output; die "$output: $error\n" };
816    
817     my $cv = AE::cv ();
818     my $log;
819     my $process = eval {
820     AnyEvent::Util::run_cmd ([
821     "patch", "--verbose", "--batch", "--forward", "--binary", "--fuzz=0",
822     "--no-backup-if-mismatch", "--reject-file=-", "--output=$output", "--", $input,
823     ],
824     "<" => \$normalized,
825     ">" => \$log,
826     close_all => 1,
827     on_prepare => sub {
828     $ENV{LC_ALL} = "C";
829     chdir $directory or die "$directory: $!\n";
830     defined POSIX::dup2 (1, 2) or die "dup2: $!\n";
831     },
832     );
833     };
834    
835     if ($@) {
836     my $error = $@;
837     unlink $output;
838     die $error;
839     }
840    
841     $process->cb (sub {
842     my $hunks;
843     eval { $hunks = check_patch_output $_[0]->recv, $log, $max_offset; 1 } or do {
844     my $error = $@;
845     unlink $output;
846     $cv->croak ($error);
847     return;
848     };
849    
850     $cv->send ($hunks);
851     });
852    
853     $cv
854     }
855    
856 root 1.1 sub _clean_text($) {
857     my ($text) = @_;
858    
859     my ($prefix) = (join "\n", grep /\S/, split /\n/, $text)
860     =~ /\A([ \t]*)[^\n]*(?:\n\1[^\n]*)*\z/;
861    
862     $text =~ s/^\Q$prefix\E//mg;
863     $text =~ s/\A\s+//;
864     $text =~ s/\s+\z//;
865     $text
866     }
867    
868     package AnyEvent::MCP::Base;
869    
870     sub new {
871     my ($class, %kv) = @_;
872     bless \%kv, $class
873     }
874    
875     sub event {
876     my ($self, $event, @args) = @_;
877     ($self->{"on_$event"} or return)->($self, @args)
878     }
879    
880     =head1 SEE ALSO
881    
882     L<AnyEvent>, L<https://modelcontextprotocol.io/>
883    
884     =head1 AUTHOR
885    
886     The main author of this module is OpenAI ChatGPT.
887    
888     Additional guidance received by:
889    
890     Marc Lehmann <schmorp@schmorp.de>
891     http://home.schmorp.de/
892    
893     =cut
894    
895     1
896