ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Client/JSONL.pm
Revision: 1.4
Committed: Tue Oct 13 10:17:31 2026 UTC (2 weeks ago) by root
Branch: MAIN
Changes since 1.3: +8 -6 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     my $client = AnyEvent::MCP::Client::JSONL->new (
8     command => [$program, @args],
9     env => { PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL => "stdio" },
10     );
11    
12 root 1.2 =head1 DESCRIPTION
13    
14     Use this transport when the MCP server is a subprocess or an already
15     connected byte stream. It reads one JSON-RPC message per line, so stdout
16     from a child server must contain protocol messages only. Diagnostic output
17     belongs on stderr. A single stray banner on stdout can break the connection.
18    
19     An argument array executes the program directly; a command string runs
20     through a shell. Prefer the array form when no shell syntax is needed.
21     The process starts on the first connection, not when the client object
22     is constructed.
23    
24 root 1.1 =head1 METHODS
25    
26     =head2 $client = AnyEvent::MCP::Client::JSONL->new (%config)
27    
28     C<command> is an argument array for direct execution, or a string explicitly
29     run through C</bin/sh -c>. The child inherits stderr. C<env> contains child
30     environment overrides. No command runs until connecting.
31    
32     C<handle> contains AnyEvent::Handle options, excluding the filehandle and
33     read/error/EOF callbacks managed here. C<max_size> bounds an incoming JSON
34     line, defaulting to 16 MiB.
35    
36     C<name> is available for application identification but does not load transport
37     defaults. The library does not infer a subprocess command or environment.
38    
39     =head2 $client->attach ($fh_in, $fh_out)
40    
41     Uses existing handles instead of a command. C<$fh_out> defaults to C<$fh_in>
42     for a bidirectional socket. The client takes ownership. Attach before sending
43     requests. Closing an attached connection requires attaching new handles before
44     reconnecting.
45    
46     Closing a subprocess connection sends SIGTERM and keeps its child watcher
47     alive until the child has been reaped. It does not restart an outstanding call.
48 root 1.3 An accepted multibroker idle shutdown is the exception: its child can exit
49     and perform cleanup without receiving SIGTERM.
50 root 1.1
51     =cut
52    
53     package AnyEvent::MCP::Client::JSONL;
54    
55     use common::sense;
56     use parent "AnyEvent::MCP::Client";
57     use AnyEvent::Handle ();
58     use AnyEvent::Util ();
59     use JSON::XS ();
60     use Scalar::Util qw(weaken);
61    
62     sub attach {
63     my ($self, $fh_in, $fh_out) = @_;
64    
65     $self->close if $self->{read_handle};
66     weaken $self;
67     $self->{read_handle} = AnyEvent::Handle->new (
68     %{$self->{handle} || {}},
69 root 1.4 fh => $fh_in,
70 root 1.1 rbuf_max => $self->{max_size} || 16 * 1024 * 1024,
71 root 1.4 on_read => undef,
72 root 1.1 on_error => sub { $self->close ($_[2]) if $self },
73 root 1.4 on_eof => sub { $self->close ("MCP stream closed") if $self },
74 root 1.1 );
75     $self->{write_handle} = $fh_out ? AnyEvent::Handle->new (
76     %{$self->{handle} || {}},
77 root 1.4 fh => $fh_out,
78     on_read => undef,
79 root 1.1 on_error => sub { $self->close ($_[2]) if $self },
80 root 1.4 on_eof => sub { $self->close ("MCP output closed") if $self },
81 root 1.1 ) : $self->{read_handle};
82     $self->_read;
83     }
84    
85     sub _open {
86     my ($self, $done) = @_;
87    
88     return $done->(1, undef) if $self->{read_handle};
89     return $done->(0, { transport => 1, message => "No command or attached stream" })
90     unless $self->{command};
91    
92     my $ok = eval {
93     local %ENV = (%ENV, %{$self->{env} || {}});
94     my ($parent, $child) = AnyEvent::Util::portable_socketpair;
95     die "Cannot create MCP socketpair: $!\n" unless defined $parent && defined $child;
96    
97     my $pid;
98     my $cv = AnyEvent::Util::run_cmd (
99     ref $self->{command} eq "ARRAY" ? $self->{command} : ["/bin/sh", "-c", $self->{command}],
100     "<" => $child, ">" => $child, '$$' => \$pid, close_all => 1,
101     );
102     close $child;
103     $self->{pid} = $pid;
104     $self->attach ($parent);
105    
106     weaken $self;
107     $cv->cb (sub {
108     my $status = $_[0]->recv;
109     if ($self && $self->{pid} == $pid) {
110     delete $self->{pid};
111     delete $self->{child};
112     $self->close ("MCP child exited (status $status)");
113     }
114     });
115     $self->{child} = $cv;
116     1
117     };
118     $done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" }));
119     }
120    
121     sub _read {
122     my ($self) = @_;
123 root 1.4
124 root 1.1 weaken $self;
125    
126     $self->{read_handle}->push_read (line => sub {
127     my ($handle, $line) = @_;
128 root 1.4
129 root 1.1 return unless $self;
130     my $ok = eval {
131     $self->_receive (JSON::XS::decode_json $line);
132     1
133     };
134     return $self->close ("$@") unless $ok;
135     $self->_read if $self->{read_handle} && !$handle->destroyed;
136     });
137     }
138    
139     sub _send {
140     my ($self, $message, $done) = @_;
141    
142     my $ok = eval {
143     my $handle = $self->{write_handle} or die "MCP stream is not connected\n";
144     $handle->push_write (JSON::XS::encode_json ($message) . "\n");
145     1
146     };
147     $done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" }));
148     }
149    
150     sub _shutdown {
151     my ($self) = @_;
152    
153     my $read = delete $self->{read_handle};
154     my $write = delete $self->{write_handle};
155     $read->destroy if $read;
156     $write->destroy if $write && $write != $read;
157     if (my $pid = delete $self->{pid}) {
158 root 1.3 kill TERM => $pid unless $self->{idle_shutdown_expected};
159 root 1.1 }
160     delete $self->{child};
161     }
162    
163     1