| 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 |
|
|
Uses existing handles instead of a command. C<$fh_out> defaults to C<$fh_in> |
| 69 |
|
|
for a bidirectional socket. The client takes ownership. Attach before sending |
| 70 |
|
|
requests. Closing an attached connection requires attaching new handles before |
| 71 |
|
|
reconnecting. |
| 72 |
|
|
|
| 73 |
|
|
Closing a subprocess connection sends SIGTERM and keeps its child watcher |
| 74 |
|
|
alive until the child has been reaped. It does not restart an outstanding call. |
| 75 |
root |
1.3 |
An accepted multibroker idle shutdown is the exception: its child can exit |
| 76 |
|
|
and perform cleanup without receiving SIGTERM. |
| 77 |
root |
1.1 |
|
| 78 |
root |
1.6 |
=back |
| 79 |
|
|
|
| 80 |
root |
1.1 |
=cut |
| 81 |
|
|
|
| 82 |
|
|
package AnyEvent::MCP::Client::JSONL; |
| 83 |
|
|
|
| 84 |
|
|
use common::sense; |
| 85 |
|
|
use parent "AnyEvent::MCP::Client"; |
| 86 |
|
|
use AnyEvent::Handle (); |
| 87 |
|
|
use AnyEvent::Util (); |
| 88 |
|
|
use JSON::XS (); |
| 89 |
|
|
use Scalar::Util qw(weaken); |
| 90 |
|
|
|
| 91 |
|
|
sub attach { |
| 92 |
|
|
my ($self, $fh_in, $fh_out) = @_; |
| 93 |
|
|
|
| 94 |
|
|
$self->close if $self->{read_handle}; |
| 95 |
|
|
weaken $self; |
| 96 |
|
|
$self->{read_handle} = AnyEvent::Handle->new ( |
| 97 |
|
|
%{$self->{handle} || {}}, |
| 98 |
root |
1.4 |
fh => $fh_in, |
| 99 |
root |
1.1 |
rbuf_max => $self->{max_size} || 16 * 1024 * 1024, |
| 100 |
root |
1.4 |
on_read => undef, |
| 101 |
root |
1.1 |
on_error => sub { $self->close ($_[2]) if $self }, |
| 102 |
root |
1.4 |
on_eof => sub { $self->close ("MCP stream closed") if $self }, |
| 103 |
root |
1.1 |
); |
| 104 |
|
|
$self->{write_handle} = $fh_out ? AnyEvent::Handle->new ( |
| 105 |
|
|
%{$self->{handle} || {}}, |
| 106 |
root |
1.4 |
fh => $fh_out, |
| 107 |
|
|
on_read => undef, |
| 108 |
root |
1.1 |
on_error => sub { $self->close ($_[2]) if $self }, |
| 109 |
root |
1.4 |
on_eof => sub { $self->close ("MCP output closed") if $self }, |
| 110 |
root |
1.1 |
) : $self->{read_handle}; |
| 111 |
|
|
$self->_read; |
| 112 |
|
|
} |
| 113 |
|
|
|
| 114 |
|
|
sub _open { |
| 115 |
|
|
my ($self, $done) = @_; |
| 116 |
|
|
|
| 117 |
|
|
return $done->(1, undef) if $self->{read_handle}; |
| 118 |
|
|
return $done->(0, { transport => 1, message => "No command or attached stream" }) |
| 119 |
|
|
unless $self->{command}; |
| 120 |
|
|
|
| 121 |
|
|
my $ok = eval { |
| 122 |
|
|
local %ENV = (%ENV, %{$self->{env} || {}}); |
| 123 |
|
|
my ($parent, $child) = AnyEvent::Util::portable_socketpair; |
| 124 |
|
|
die "Cannot create MCP socketpair: $!\n" unless defined $parent && defined $child; |
| 125 |
|
|
|
| 126 |
|
|
my $pid; |
| 127 |
|
|
my $cv = AnyEvent::Util::run_cmd ( |
| 128 |
|
|
ref $self->{command} eq "ARRAY" ? $self->{command} : ["/bin/sh", "-c", $self->{command}], |
| 129 |
|
|
"<" => $child, ">" => $child, '$$' => \$pid, close_all => 1, |
| 130 |
|
|
); |
| 131 |
|
|
close $child; |
| 132 |
|
|
$self->{pid} = $pid; |
| 133 |
|
|
$self->attach ($parent); |
| 134 |
|
|
|
| 135 |
|
|
weaken $self; |
| 136 |
|
|
$cv->cb (sub { |
| 137 |
|
|
my $status = $_[0]->recv; |
| 138 |
|
|
if ($self && $self->{pid} == $pid) { |
| 139 |
|
|
delete $self->{pid}; |
| 140 |
|
|
delete $self->{child}; |
| 141 |
|
|
$self->close ("MCP child exited (status $status)"); |
| 142 |
|
|
} |
| 143 |
|
|
}); |
| 144 |
|
|
$self->{child} = $cv; |
| 145 |
|
|
1 |
| 146 |
|
|
}; |
| 147 |
|
|
$done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" })); |
| 148 |
|
|
} |
| 149 |
|
|
|
| 150 |
|
|
sub _read { |
| 151 |
|
|
my ($self) = @_; |
| 152 |
root |
1.4 |
|
| 153 |
root |
1.1 |
weaken $self; |
| 154 |
|
|
|
| 155 |
|
|
$self->{read_handle}->push_read (line => sub { |
| 156 |
|
|
my ($handle, $line) = @_; |
| 157 |
root |
1.4 |
|
| 158 |
root |
1.1 |
return unless $self; |
| 159 |
|
|
my $ok = eval { |
| 160 |
|
|
$self->_receive (JSON::XS::decode_json $line); |
| 161 |
|
|
1 |
| 162 |
|
|
}; |
| 163 |
|
|
return $self->close ("$@") unless $ok; |
| 164 |
|
|
$self->_read if $self->{read_handle} && !$handle->destroyed; |
| 165 |
|
|
}); |
| 166 |
|
|
} |
| 167 |
|
|
|
| 168 |
|
|
sub _send { |
| 169 |
|
|
my ($self, $message, $done) = @_; |
| 170 |
|
|
|
| 171 |
|
|
my $ok = eval { |
| 172 |
|
|
my $handle = $self->{write_handle} or die "MCP stream is not connected\n"; |
| 173 |
|
|
$handle->push_write (JSON::XS::encode_json ($message) . "\n"); |
| 174 |
|
|
1 |
| 175 |
|
|
}; |
| 176 |
|
|
$done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" })); |
| 177 |
|
|
} |
| 178 |
|
|
|
| 179 |
|
|
sub _shutdown { |
| 180 |
|
|
my ($self) = @_; |
| 181 |
|
|
|
| 182 |
|
|
my $read = delete $self->{read_handle}; |
| 183 |
|
|
my $write = delete $self->{write_handle}; |
| 184 |
|
|
$read->destroy if $read; |
| 185 |
|
|
$write->destroy if $write && $write != $read; |
| 186 |
|
|
if (my $pid = delete $self->{pid}) { |
| 187 |
root |
1.3 |
kill TERM => $pid unless $self->{idle_shutdown_expected}; |
| 188 |
root |
1.1 |
} |
| 189 |
|
|
delete $self->{child}; |
| 190 |
|
|
} |
| 191 |
|
|
|
| 192 |
|
|
1 |