ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/lib/AnyEvent/MCP/Server/JSONL.pm
Revision: 1.6
Committed: Tue Oct 13 22:54:22 2026 UTC (2 weeks ago) by root
Branch: MAIN
Changes since 1.5: +29 -10 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Server::JSONL - MCP over a newline-delimited byte stream
4    
5     =head1 DESCRIPTION
6    
7 root 1.2 Use this transport for stdio servers or an existing stream connection.
8     It implements L<AnyEvent::MCP::Server> over newline-delimited UTF-8 JSON-RPC:
9     one line is one complete message. Keep diagnostic output off the protocol
10     stream; a banner or log message on stdout is not a JSON-RPC response.
11    
12     Unlike HTTP, replies can complete in a different order from requests.
13     The JSON-RPC ids associate them with the correct calls.
14 root 1.1
15     =head1 METHODS
16    
17 root 1.6 =over
18    
19     =item $handle = $server->accept ($fh, $fh_out)
20 root 1.1
21     Starts handling C<$fh>. If C<$fh_out> is supplied, it receives outgoing
22     messages; otherwise C<$fh> is used in both directions. Both handles belong
23     to the server until the connection closes.
24    
25     With separate input/output handles, input EOF waits for pending replies
26     and drains the output before closing. EOF on a bidirectional socket closes
27     it immediately and cancels outstanding requests.
28    
29     C<max_size> limits a buffered incoming message, defaulting to 1 MiB.
30    
31 root 1.6 =item $server->close
32 root 1.1
33     Closes the server's connections and cancels outstanding requests.
34    
35 root 1.6 =back
36    
37 root 1.1 =head1 EVENTS
38    
39 root 1.6 =over
40    
41     =item on_connection_error => $callback->($server, $handle, $message)
42 root 1.1
43     Called for stream errors before closing the connection.
44    
45 root 1.6 =item on_connection_close => $callback->($server, $handle)
46 root 1.1
47     Called after a connection has closed.
48    
49 root 1.6 =item on_close => $callback->($server)
50 root 1.1
51     Called by C<close> after closing all connections.
52    
53 root 1.6 =back
54    
55 root 1.5 =head1 MANAGED SHUTDOWN
56    
57 root 1.6 =over
58    
59     =item $server->request_idle_exit
60 root 1.3
61     Requests a managed shutdown, once, from a spawned stdio multibroker child
62     whose peer advertised C<anyevent-mcp/multibroker> version 1. The application
63 root 1.6 decides when it is idle. The method returns true when it sends the notification, or undef when
64     there is no eligible connection or it has already sent one.
65     The broker can ignore the notification. When it sends shutdown, the server
66     answers before draining the stream and closing its connections. Closing
67     the server lets Simple's blocking starter return; an embedding application
68     controls its own process lifetime.
69    
70     C<on_idle_shutdown>, if supplied, is C<$callback->($server)> returning true
71     to accept or false to decline a late shutdown request (for example, if
72     application work has resumed). Outstanding MCP requests also prevent
73     acceptance.
74    
75     =back
76 root 1.3
77 root 1.1 =cut
78    
79     package AnyEvent::MCP::Server::JSONL;
80    
81     use common::sense;
82     use parent "AnyEvent::MCP::Server";
83     use AnyEvent::Handle ();
84 root 1.3 use AnyEvent ();
85     use JSON::XS ();
86 root 1.1 use Scalar::Util qw(weaken);
87    
88     sub accept {
89     my ($self, $fh, $fh_out) = @_;
90    
91     delete $self->{_closed};
92     weaken $self;
93    
94     my $handle = AnyEvent::Handle->new (
95     fh => $fh,
96     rbuf_max => $self->{max_size} || 1024 * 1024,
97     on_read => sub { $self->_read ($_[0]) if $self },
98     on_error => sub {
99     my ($handle, $fatal, $message) = @_;
100    
101     $self->event (connection_error => $handle, $message) if $self;
102     $self->_close ($handle) if $self;
103     },
104     on_eof => sub {
105     return unless $self;
106     my ($handle) = @_;
107    
108     if ($handle->{mcp_out}) {
109     $handle->{mcp_eof} = 1;
110     $self->_drain ($handle);
111     } else {
112     $self->_close ($handle);
113     }
114     },
115     );
116    
117     if ($fh_out) {
118     $handle->{mcp_out} = AnyEvent::Handle->new (
119     fh => $fh_out,
120     on_error => sub {
121     $self->event (connection_error => $_[0], $_[2]) if $self;
122     $self->close if $self;
123     },
124     );
125     }
126    
127     $handle->{mcp_peer} = {};
128 root 1.3 $handle->{mcp_peer}{mcp_handle} = $handle;
129     weaken $handle->{mcp_peer}{mcp_handle};
130 root 1.1 $self->{connections}{$handle} = $handle;
131    
132     $handle
133     }
134    
135     sub _read {
136     my ($self, $handle) = @_;
137    
138     weaken $self;
139    
140     $handle->push_read (line => sub {
141     my ($handle, $line) = @_;
142 root 1.4
143 root 1.1 return unless $self;
144    
145     ++$handle->{mcp_active};
146     $self->json_rpc ($line, sub {
147     --$handle->{mcp_active};
148     return if $handle->{mcp_closed};
149    
150     ($handle->{mcp_out} || $handle)->push_write ($_[0] . "\n")
151     if defined $_[0];
152     $self->_drain ($handle) if $handle->{mcp_eof};
153     },
154 root 1.4 peer => $handle->{mcp_peer},
155 root 1.1 on_notify => sub {
156     return if $handle->{mcp_closed};
157    
158     ($handle->{mcp_out} || $handle)->push_write (JSON::XS::encode_json ($_[1]) . "\n");
159     },
160     );
161     });
162     }
163    
164 root 1.3 sub request_idle_exit {
165     my ($self) = @_;
166    
167     return unless $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio"
168     && !$self->{idle_exit_requested};
169    
170     for my $handle (values %{$self->{connections} || {}}) {
171     next if $handle->{mcp_closed} || !$handle->{mcp_peer}{aemcp_multibroker};
172    
173     ($handle->{mcp_out} || $handle)->push_write (JSON::XS::encode_json ({
174     jsonrpc => "2.0", method => "anyevent-mcp/idle-exit", params => {},
175     }) . "\n");
176     $self->{idle_exit_requested} = 1;
177     return 1
178     }
179    
180     undef
181     }
182    
183     sub dispatch {
184     my ($self, $req) = @_;
185    
186     if ($req->{method} eq "anyevent-mcp/shutdown") {
187     my $handle = $req->{peer}{mcp_handle};
188     my $busy = grep $_->{mcp_active} > ($_ == $handle ? 1 : 0),
189     values %{$self->{connections} || {}};
190     my $accepted = $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio"
191     && $req->{peer}{aemcp_multibroker} && $self->{idle_exit_requested}
192     && !$self->{_idle_shutting_down} && !$busy
193     && (!$self->{on_idle_shutdown} || $self->{on_idle_shutdown}->($self));
194    
195     $req->reply ({ accepted => $accepted ? JSON::XS::true : JSON::XS::false });
196     if ($accepted) {
197     $self->{_idle_shutting_down} = 1;
198     my $out = $handle->{mcp_out} || $handle;
199     weaken $self;
200     if (length $out->{wbuf}) {
201     $out->on_drain (sub { $self->close if $self });
202     } else {
203     AnyEvent::postpone (sub { $self->close if $self });
204     }
205     }
206     return
207     }
208    
209     $self->SUPER::dispatch ($req)
210     }
211    
212 root 1.1 sub _drain {
213     my ($self, $handle) = @_;
214    
215     return if $handle->{mcp_active} || $handle->{mcp_closed};
216     my $output = $handle->{mcp_out};
217     if (length $output->{wbuf}) {
218     weaken $self;
219     $output->on_drain (sub { $self->_close ($handle) if $self });
220     } else {
221     $self->_close ($handle);
222     }
223     }
224    
225     sub _close {
226     my ($self, $handle) = @_;
227    
228     return if $handle->{mcp_closed};
229     $handle->{mcp_closed} = 1;
230    
231     my $requests = delete $handle->{mcp_peer}{requests} || {};
232     $_->cancel ("Connection closed") for grep defined, values %$requests;
233    
234     delete $self->{connections}{$handle};
235     $handle->{mcp_out}->destroy if $handle->{mcp_out};
236     $handle->destroy;
237     $self->event (connection_close => $handle);
238     }
239    
240     sub close {
241     my ($self) = @_;
242    
243     return if $self->{_closed}++;
244     delete $self->{listener};
245     $self->_close ($_) for values %{$self->{connections} || {}};
246     $self->event (close =>);
247     }
248    
249     sub DESTROY {
250     $_[0]->close
251     }
252    
253     1