=head1 NAME AnyEvent::MCP::Server::JSONL - MCP over a newline-delimited byte stream =head1 DESCRIPTION Use this transport for stdio servers or an existing stream connection. It implements L over newline-delimited UTF-8 JSON-RPC: one line is one complete message. Keep diagnostic output off the protocol stream; a banner or log message on stdout is not a JSON-RPC response. Unlike HTTP, replies can complete in a different order from requests. The JSON-RPC ids associate them with the correct calls. =head1 METHODS =head2 $handle = $server->accept ($fh, $fh_out) Starts handling C<$fh>. If C<$fh_out> is supplied, it receives outgoing messages; otherwise C<$fh> is used in both directions. Both handles belong to the server until the connection closes. With separate input/output handles, input EOF waits for pending replies and drains the output before closing. EOF on a bidirectional socket closes it immediately and cancels outstanding requests. C limits a buffered incoming message, defaulting to 1 MiB. =head2 $server->close Closes the server's connections and cancels outstanding requests. =head1 EVENTS =head2 on_connection_error => $callback->($server, $handle, $message) Called for stream errors before closing the connection. =head2 on_connection_close => $callback->($server, $handle) Called after a connection has closed. =head2 on_close => $callback->($server) Called by C after closing all connections. =head2 $server->request_idle_exit Requests a managed shutdown, once, from a spawned stdio multibroker child whose peer advertised C version 1. The application decides when it is idle. No process exits until the broker sends shutdown. The shutdown request is explicitly answered before the stream is closed. C, if supplied, is a callback returning true to accept or false to decline a late shutdown request (e.g. if the application became busy). =cut package AnyEvent::MCP::Server::JSONL; use common::sense; use parent "AnyEvent::MCP::Server"; use AnyEvent::Handle (); use AnyEvent (); use JSON::XS (); use Scalar::Util qw(weaken); sub accept { my ($self, $fh, $fh_out) = @_; delete $self->{_closed}; weaken $self; my $handle = AnyEvent::Handle->new ( fh => $fh, rbuf_max => $self->{max_size} || 1024 * 1024, on_read => sub { $self->_read ($_[0]) if $self }, on_error => sub { my ($handle, $fatal, $message) = @_; $self->event (connection_error => $handle, $message) if $self; $self->_close ($handle) if $self; }, on_eof => sub { return unless $self; my ($handle) = @_; if ($handle->{mcp_out}) { $handle->{mcp_eof} = 1; $self->_drain ($handle); } else { $self->_close ($handle); } }, ); if ($fh_out) { $handle->{mcp_out} = AnyEvent::Handle->new ( fh => $fh_out, on_error => sub { $self->event (connection_error => $_[0], $_[2]) if $self; $self->close if $self; }, ); } $handle->{mcp_peer} = {}; $handle->{mcp_peer}{mcp_handle} = $handle; weaken $handle->{mcp_peer}{mcp_handle}; $self->{connections}{$handle} = $handle; $handle } sub _read { my ($self, $handle) = @_; weaken $self; $handle->push_read (line => sub { my ($handle, $line) = @_; return unless $self; ++$handle->{mcp_active}; $self->json_rpc ($line, sub { --$handle->{mcp_active}; return if $handle->{mcp_closed}; ($handle->{mcp_out} || $handle)->push_write ($_[0] . "\n") if defined $_[0]; $self->_drain ($handle) if $handle->{mcp_eof}; }, peer => $handle->{mcp_peer}, on_notify => sub { return if $handle->{mcp_closed}; ($handle->{mcp_out} || $handle)->push_write (JSON::XS::encode_json ($_[1]) . "\n"); }, ); }); } sub request_idle_exit { my ($self) = @_; return unless $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio" && !$self->{idle_exit_requested}; for my $handle (values %{$self->{connections} || {}}) { next if $handle->{mcp_closed} || !$handle->{mcp_peer}{aemcp_multibroker}; ($handle->{mcp_out} || $handle)->push_write (JSON::XS::encode_json ({ jsonrpc => "2.0", method => "anyevent-mcp/idle-exit", params => {}, }) . "\n"); $self->{idle_exit_requested} = 1; return 1 } undef } sub dispatch { my ($self, $req) = @_; if ($req->{method} eq "anyevent-mcp/shutdown") { my $handle = $req->{peer}{mcp_handle}; my $busy = grep $_->{mcp_active} > ($_ == $handle ? 1 : 0), values %{$self->{connections} || {}}; my $accepted = $ENV{PERL_ANYEVENT_MCP_MULTIBROKER_PROTOCOL} eq "stdio" && $req->{peer}{aemcp_multibroker} && $self->{idle_exit_requested} && !$self->{_idle_shutting_down} && !$busy && (!$self->{on_idle_shutdown} || $self->{on_idle_shutdown}->($self)); $req->reply ({ accepted => $accepted ? JSON::XS::true : JSON::XS::false }); if ($accepted) { $self->{_idle_shutting_down} = 1; my $out = $handle->{mcp_out} || $handle; weaken $self; if (length $out->{wbuf}) { $out->on_drain (sub { $self->close if $self }); } else { AnyEvent::postpone (sub { $self->close if $self }); } } return } $self->SUPER::dispatch ($req) } sub _drain { my ($self, $handle) = @_; return if $handle->{mcp_active} || $handle->{mcp_closed}; my $output = $handle->{mcp_out}; if (length $output->{wbuf}) { weaken $self; $output->on_drain (sub { $self->_close ($handle) if $self }); } else { $self->_close ($handle); } } sub _close { my ($self, $handle) = @_; return if $handle->{mcp_closed}; $handle->{mcp_closed} = 1; my $requests = delete $handle->{mcp_peer}{requests} || {}; $_->cancel ("Connection closed") for grep defined, values %$requests; delete $self->{connections}{$handle}; $handle->{mcp_out}->destroy if $handle->{mcp_out}; $handle->destroy; $self->event (connection_close => $handle); } sub close { my ($self) = @_; return if $self->{_closed}++; delete $self->{listener}; $self->_close ($_) for values %{$self->{connections} || {}}; $self->event (close =>); } sub DESTROY { $_[0]->close } 1