=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. =cut package AnyEvent::MCP::Server::JSONL; use common::sense; use parent "AnyEvent::MCP::Server"; use AnyEvent::Handle (); 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} = {}; $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 _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