ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Client/JSONL.pm
Revision: 1.3
Committed: Mon Oct 12 05:03:59 2026 UTC (2 weeks, 1 day ago) by root
Branch: MAIN
Changes since 1.2: +3 -1 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     fh => $fh_in,
70     rbuf_max => $self->{max_size} || 16 * 1024 * 1024,
71     on_read => undef,
72     on_error => sub { $self->close ($_[2]) if $self },
73     on_eof => sub { $self->close ("MCP stream closed") if $self },
74     );
75     $self->{write_handle} = $fh_out ? AnyEvent::Handle->new (
76     %{$self->{handle} || {}},
77     fh => $fh_out,
78     on_read => undef,
79     on_error => sub { $self->close ($_[2]) if $self },
80     on_eof => sub { $self->close ("MCP output closed") if $self },
81     ) : $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     weaken $self;
124    
125     $self->{read_handle}->push_read (line => sub {
126     my ($handle, $line) = @_;
127     return unless $self;
128     my $ok = eval {
129     $self->_receive (JSON::XS::decode_json $line);
130     1
131     };
132     return $self->close ("$@") unless $ok;
133     $self->_read if $self->{read_handle} && !$handle->destroyed;
134     });
135     }
136    
137     sub _send {
138     my ($self, $message, $done) = @_;
139    
140     my $ok = eval {
141     my $handle = $self->{write_handle} or die "MCP stream is not connected\n";
142     $handle->push_write (JSON::XS::encode_json ($message) . "\n");
143     1
144     };
145     $done->($ok ? (1, undef) : (0, { transport => 1, message => "$@" }));
146     }
147    
148     sub _shutdown {
149     my ($self) = @_;
150    
151     my $read = delete $self->{read_handle};
152     my $write = delete $self->{write_handle};
153     $read->destroy if $read;
154     $write->destroy if $write && $write != $read;
155     if (my $pid = delete $self->{pid}) {
156 root 1.3 kill TERM => $pid unless $self->{idle_shutdown_expected};
157 root 1.1 }
158     delete $self->{child};
159     }
160    
161     1