ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Client/JSONL.pm
Revision: 1.7
Committed: Thu Oct 15 05:48:13 2026 UTC (12 days, 8 hours ago) by root
Branch: MAIN
Changes since 1.6: +9 -10 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Client::JSONL - MCP byte streams and stdio subprocesses
4    
5     =head1 SYNOPSIS
6    
7 root 1.6 use AnyEvent::MCP::Client::JSONL;
8    
9 root 1.1 my $client = AnyEvent::MCP::Client::JSONL->new (
10     command => [$program, @args],
11     env => { PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL => "stdio" },
12     );
13    
14 root 1.2 =head1 DESCRIPTION
15    
16     Use this transport when the MCP server is a subprocess or an already
17     connected byte stream. It reads one JSON-RPC message per line, so stdout
18 root 1.5 on a conventional stdio transport must contain protocol messages only.
19     The C<command> mode here connects the child's stdin and stdout to a
20     socketpair; diagnostic output belongs on stderr. A stray banner or log
21     message on the protocol stream can break the connection.
22 root 1.2
23     An argument array executes the program directly; a command string runs
24     through a shell. Prefer the array form when no shell syntax is needed.
25     The process starts on the first connection, not when the client object
26     is constructed.
27    
28 root 1.1 =head1 METHODS
29    
30 root 1.6 =over
31    
32     =item $client = AnyEvent::MCP::Client::JSONL->new (%config)
33    
34     Accepts the common L<AnyEvent::MCP::Client> options and these transport
35     options:
36    
37     =over
38    
39     =item command => [$program, $arg...]
40    
41     =item command => $shell_command
42    
43     Argument array for direct execution, or a string run through C</bin/sh -c>.
44     The child inherits stderr. No command runs until connecting.
45     Omit this option when using C<attach> instead.
46 root 1.1
47 root 1.6 =item env => { %environment }
48 root 1.1
49 root 1.6 Child environment overrides. Unspecified variables are inherited from
50     the parent; the parent environment is unchanged.
51 root 1.1
52 root 1.6 =item handle => { %handle }
53 root 1.1
54 root 1.6 L<AnyEvent::Handle> options. The filehandle and read/error/EOF callbacks
55     are managed by this transport.
56    
57     =item max_size => $bytes
58    
59     Bounds a buffered incoming JSON line, defaulting to 16 MiB.
60    
61     =back
62    
63     C<name> does not load transport defaults. The library does not infer a
64     subprocess command or environment.
65    
66     =item $client->attach ($fh_in, $fh_out)
67 root 1.1
68 root 1.7 Uses existing handles instead of a command. C<$fh_in> may also be an
69     L<AnyEvent::Handle>, whose buffered input is preserved. C<$fh_out> defaults to C<$fh_in>
70 root 1.1 for a bidirectional socket. The client takes ownership. Attach before sending
71     requests. Closing an attached connection requires attaching new handles before
72     reconnecting.
73    
74     Closing a subprocess connection sends SIGTERM and keeps its child watcher
75     alive until the child has been reaped. It does not restart an outstanding call.
76 root 1.3 An accepted multibroker idle shutdown is the exception: its child can exit
77     and perform cleanup without receiving SIGTERM.
78 root 1.1
79 root 1.6 =back
80    
81 root 1.1 =cut
82    
83     package AnyEvent::MCP::Client::JSONL;
84    
85     use common::sense;
86     use parent "AnyEvent::MCP::Client";
87     use AnyEvent::Handle ();
88     use AnyEvent::Util ();
89     use JSON::XS ();
90 root 1.7 use Scalar::Util qw(weaken blessed);
91 root 1.1
92     sub attach {
93     my ($self, $fh_in, $fh_out) = @_;
94    
95     $self->close if $self->{read_handle};
96     weaken $self;
97 root 1.7 $self->{read_handle} = blessed $fh_in && $fh_in->isa ("AnyEvent::Handle")
98     ? $fh_in : AnyEvent::Handle->new (%{$self->{handle} || {}}, fh => $fh_in);
99     $self->{read_handle}{rbuf_max} = $self->{max_size} || 16 * 1024 * 1024;
100     $self->{read_handle}->on_read (undef);
101     $self->{read_handle}->on_error (sub { $self->close ($_[2]) if $self });
102     $self->{read_handle}->on_eof (sub { $self->close ("MCP stream closed") if $self });
103 root 1.1 $self->{write_handle} = $fh_out ? AnyEvent::Handle->new (
104     %{$self->{handle} || {}},
105 root 1.4 fh => $fh_out,
106     on_read => undef,
107 root 1.1 on_error => sub { $self->close ($_[2]) if $self },
108 root 1.4 on_eof => sub { $self->close ("MCP output closed") if $self },
109 root 1.1 ) : $self->{read_handle};
110     $self->_read;
111     }
112    
113     sub _open {
114     my ($self, $done) = @_;
115    
116     return $done->(1, undef) if $self->{read_handle};
117     return $done->(0, { transport => 1, message => "No command or attached stream" })
118     unless $self->{command};
119    
120     my $ok = eval {
121     local %ENV = (%ENV, %{$self->{env} || {}});
122     my ($parent, $child) = AnyEvent::Util::portable_socketpair;
123     die "Cannot create MCP socketpair: $!\n" unless defined $parent && defined $child;
124    
125     my $pid;
126     my $cv = AnyEvent::Util::run_cmd (
127     ref $self->{command} eq "ARRAY" ? $self->{command} : ["/bin/sh", "-c", $self->{command}],
128     "<" => $child, ">" => $child, '$$' => \$pid, close_all => 1,
129     );
130     close $child;
131     $self->{pid} = $pid;
132     $self->attach ($parent);
133    
134     weaken $self;
135     $cv->cb (sub {
136     my $status = $_[0]->recv;
137     if ($self && $self->{pid} == $pid) {
138     delete $self->{pid};
139     delete $self->{child};
140     $self->close ("MCP child exited (status $status)");
141     }
142     });
143     $self->{child} = $cv;
144     1
145     };
146     $done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" }));
147     }
148    
149     sub _read {
150     my ($self) = @_;
151 root 1.4
152 root 1.1 weaken $self;
153    
154     $self->{read_handle}->push_read (line => sub {
155     my ($handle, $line) = @_;
156 root 1.4
157 root 1.1 return unless $self;
158     my $ok = eval {
159     $self->_receive (JSON::XS::decode_json $line);
160     1
161     };
162     return $self->close ("$@") unless $ok;
163     $self->_read if $self->{read_handle} && !$handle->destroyed;
164     });
165     }
166    
167     sub _send {
168     my ($self, $message, $done) = @_;
169    
170     my $ok = eval {
171     my $handle = $self->{write_handle} or die "MCP stream is not connected\n";
172     $handle->push_write (JSON::XS::encode_json ($message) . "\n");
173     1
174     };
175     $done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" }));
176     }
177    
178     sub _shutdown {
179     my ($self) = @_;
180    
181     my $read = delete $self->{read_handle};
182     my $write = delete $self->{write_handle};
183     $read->destroy if $read;
184     $write->destroy if $write && $write != $read;
185     if (my $pid = delete $self->{pid}) {
186 root 1.3 kill TERM => $pid unless $self->{idle_shutdown_expected};
187 root 1.1 }
188     delete $self->{child};
189     }
190    
191     1