Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package perl-MCP for openSUSE:Factory 
checked in at 2026-05-18 17:48:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/perl-MCP (Old)
 and      /work/SRC/openSUSE:Factory/.perl-MCP.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "perl-MCP"

Mon May 18 17:48:00 2026 rev:6 rq:1353756 version:0.100.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/perl-MCP/perl-MCP.changes        2026-02-19 
14:25:14.631362739 +0100
+++ /work/SRC/openSUSE:Factory/.perl-MCP.new.1966/perl-MCP.changes      
2026-05-18 17:48:47.234212894 +0200
@@ -1,0 +2,22 @@
+Thu May  7 08:03:53 UTC 2026 - Tina Müller <[email protected]>
+
+- updated to 0.100.0 (0.10)
+   see /usr/share/doc/packages/perl-MCP/Changes
+
+  0.10  2026-05-06
+    - Added opt-in server-to-client streaming and session termination to the 
HTTP transport. Not compatible with
+      pre-forking web servers.
+    - Added support for list_changed notifications.
+    - Added support for progress notifications.
+    - Added MCP::Primitive class.
+    - Added MCP::Server::Context class.
+    - Added MCP::Server::Session class.
+    - Added heartbeat, session_timeout, sessions, and streaming attributes, 
and a notify method, to
+      MCP::Server::Transport::HTTP.
+    - Added notify method to MCP::Server::Transport::Stdio.
+    - Added notifications method to MCP::Server::Transport.
+    - Added notify_all method to MCP::Server::Transport::HTTP and 
MCP::Server::Transport::Stdio.
+    - Added notify_list_changed method to MCP::Server.
+    - Added delete_session method to MCP::Client.
+
+-------------------------------------------------------------------

Old:
----
  MCP-0.08.tar.gz

New:
----
  MCP-0.10.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ perl-MCP.spec ++++++
--- /var/tmp/diff_new_pack.helyn7/_old  2026-05-18 17:48:47.826237357 +0200
+++ /var/tmp/diff_new_pack.helyn7/_new  2026-05-18 17:48:47.830237523 +0200
@@ -18,10 +18,10 @@
 
 %define cpan_name MCP
 Name:           perl-MCP
-Version:        0.80.0
+Version:        0.100.0
 Release:        0
-# 0.08 -> normalize -> 0.80.0
-%define cpan_version 0.08
+# 0.10 -> normalize -> 0.100.0
+%define cpan_version 0.10
 License:        MIT
 Summary:        Connect Perl with AI using MCP (Model Context Protocol)
 URL:            https://metacpan.org/release/%{cpan_name}
@@ -41,9 +41,12 @@
 Provides:       perl(MCP) = %{version}
 Provides:       perl(MCP::Client)
 Provides:       perl(MCP::Constants)
+Provides:       perl(MCP::Primitive)
 Provides:       perl(MCP::Prompt)
 Provides:       perl(MCP::Resource)
 Provides:       perl(MCP::Server)
+Provides:       perl(MCP::Server::Context)
+Provides:       perl(MCP::Server::Session)
 Provides:       perl(MCP::Server::Transport)
 Provides:       perl(MCP::Server::Transport::HTTP)
 Provides:       perl(MCP::Server::Transport::Stdio)

++++++ MCP-0.08.tar.gz -> MCP-0.10.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/Changes new/MCP-0.10/Changes
--- old/MCP-0.08/Changes        2026-02-17 13:14:19.000000000 +0100
+++ new/MCP-0.10/Changes        2026-05-06 22:55:47.000000000 +0200
@@ -1,4 +1,20 @@
 
+0.10  2026-05-06
+  - Added opt-in server-to-client streaming and session termination to the 
HTTP transport. Not compatible with
+    pre-forking web servers.
+  - Added support for list_changed notifications.
+  - Added support for progress notifications.
+  - Added MCP::Primitive class.
+  - Added MCP::Server::Context class.
+  - Added MCP::Server::Session class.
+  - Added heartbeat, session_timeout, sessions, and streaming attributes, and 
a notify method, to
+    MCP::Server::Transport::HTTP.
+  - Added notify method to MCP::Server::Transport::Stdio.
+  - Added notifications method to MCP::Server::Transport.
+  - Added notify_all method to MCP::Server::Transport::HTTP and 
MCP::Server::Transport::Stdio.
+  - Added notify_list_changed method to MCP::Server.
+  - Added delete_session method to MCP::Client.
+
 0.08  2026-02-17
   - Added support for tool annotations. (d3flex)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/MANIFEST new/MCP-0.10/MANIFEST
--- old/MCP-0.08/MANIFEST       2026-02-17 13:15:49.000000000 +0100
+++ new/MCP-0.10/MANIFEST       2026-05-06 22:58:51.000000000 +0200
@@ -2,12 +2,16 @@
 Changes
 examples/echo_http.pl
 examples/echo_stdio.pl
+examples/streaming_http.pl
 lib/MCP.pm
 lib/MCP/Client.pm
 lib/MCP/Constants.pm
+lib/MCP/Primitive.pm
 lib/MCP/Prompt.pm
 lib/MCP/Resource.pm
 lib/MCP/Server.pm
+lib/MCP/Server/Context.pm
+lib/MCP/Server/Session.pm
 lib/MCP/Server/Transport.pm
 lib/MCP/Server/Transport/HTTP.pm
 lib/MCP/Server/Transport/Stdio.pm
@@ -26,5 +30,6 @@
 t/pod_coverage.t
 t/session_specific_app.t
 t/stdio.t
+t/streaming.t
 META.yml                                 Module YAML meta-data (added by 
MakeMaker)
 META.json                                Module JSON meta-data (added by 
MakeMaker)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/META.json new/MCP-0.10/META.json
--- old/MCP-0.08/META.json      2026-02-17 13:15:49.000000000 +0100
+++ new/MCP-0.10/META.json      2026-05-06 22:58:51.000000000 +0200
@@ -61,6 +61,6 @@
          "web" : "https://web.libera.chat/#mojo";
       }
    },
-   "version" : "0.08",
+   "version" : "0.10",
    "x_serialization_backend" : "JSON::PP version 4.16"
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/META.yml new/MCP-0.10/META.yml
--- old/MCP-0.08/META.yml       2026-02-17 13:15:49.000000000 +0100
+++ new/MCP-0.10/META.yml       2026-05-06 22:58:51.000000000 +0200
@@ -33,5 +33,5 @@
   homepage: https://mojolicious.org
   license: http://www.opensource.org/licenses/mit
   repository: https://github.com/mojolicious/mojo-mcp.git
-version: '0.08'
+version: '0.10'
 x_serialization_backend: 'CPAN::Meta::YAML version 0.020'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/README.md new/MCP-0.10/README.md
--- old/MCP-0.08/README.md      2025-12-04 15:39:51.000000000 +0100
+++ new/MCP-0.10/README.md      2026-05-06 22:55:47.000000000 +0200
@@ -13,6 +13,8 @@
 
   * Tool calling, prompts and resources
   * Streamable HTTP and Stdio transports
+  * Notifications for list changes (tools, prompts, resources)
+  * Progress tracking for long-running operations
   * Scalable with pre-forking web server and async tools using promises
   * HTTP client for testing
   * Can be embedded in Mojolicious web apps
@@ -51,6 +53,33 @@
 
 Authentication can be added by the web application, just like for any other 
route.
 
+## Server-to-Client Streaming
+
+The HTTP transport can optionally accept `GET` requests to open a long-lived 
SSE stream the server can push
+notifications to, and `DELETE` requests to terminate a session. This requires 
per-process state and is not
+compatible with pre-forking web servers, so it is opt-in.
+
+```perl
+use Mojolicious::Lite -signatures;
+
+use MCP::Server;
+
+my $server = MCP::Server->new;
+$server->tool(
+  name         => 'echo',
+  description  => 'Echo the input text',
+  input_schema => {type => 'object', properties => {msg => {type => 
'string'}}, required => ['msg']},
+  code         => sub ($tool, $args) {
+    $tool->context->notify('notifications/message', {level => 'info', data => 
"Echoing: $args->{msg}"});
+    return "Echo: $args->{msg}";
+  }
+);
+
+any '/mcp' => $server->to_action({streaming => 1});
+
+app->start;
+```
+
 ## Stdio Transport
 
 Build local command line applications and use the stdio transport for testing 
with the `to_stdio` method.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/examples/streaming_http.pl 
new/MCP-0.10/examples/streaming_http.pl
--- old/MCP-0.08/examples/streaming_http.pl     1970-01-01 01:00:00.000000000 
+0100
+++ new/MCP-0.10/examples/streaming_http.pl     2026-05-06 22:55:47.000000000 
+0200
@@ -0,0 +1,45 @@
+#
+# This example demonstrates progress notifications for a long-running MCP tool
+#
+# mcp.json:
+# {
+#   "mcpServers": {
+#     "mojo": {
+#       "url": "http://127.0.0.1:3000/mcp";
+#     }
+#   }
+# }
+#
+use Mojolicious::Lite -signatures;
+
+use MCP::Server;
+use Mojo::IOLoop;
+use Mojo::Promise;
+
+my $server = MCP::Server->new;
+$server->tool(
+  name         => 'process_items',
+  description  => 'Process a number of items and report progress along the 
way',
+  input_schema => {type => 'object', properties => {items => {type => 
'integer'}}},
+  code         => sub ($tool, $args) {
+    my $context = $tool->context;
+    my $total   = $args->{items} || 5;
+    my $promise = Mojo::Promise->new;
+    my $done    = 0;
+    my $id;
+    $id = Mojo::IOLoop->recurring(
+      0.5 => sub {
+        $done++;
+        $context->notify_progress($done, $total, "Processed item $done of 
$total");
+        return if $done < $total;
+        Mojo::IOLoop->remove($id);
+        $promise->resolve("Processed $total items");
+      }
+    );
+    return $promise;
+  }
+);
+
+any '/mcp' => $server->to_action({streaming => 1});
+
+app->start;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Client.pm 
new/MCP-0.10/lib/MCP/Client.pm
--- old/MCP-0.08/lib/MCP/Client.pm      2025-12-04 15:39:51.000000000 +0100
+++ new/MCP-0.10/lib/MCP/Client.pm      2026-05-06 21:53:53.000000000 +0200
@@ -28,6 +28,18 @@
   return _result($self->send_request($request));
 }
 
+sub delete_session ($self) {
+  return undef unless my $session_id = $self->session_id;
+  my $tx = $self->ua->build_tx(DELETE => $self->url => {'Mcp-Session-Id' => 
$session_id});
+  $tx = $self->ua->start($tx);
+  if (my $err = $tx->error) {
+    croak "$err->{code} response: $err->{message}" if $err->{code};
+    croak "Connection error: $err->{message}";
+  }
+  $self->session_id(undef);
+  return 1;
+}
+
 sub get_prompt ($self, $name, $args = {}) {
   my $request = $self->build_request('prompts/get', {name => $name, arguments 
=> $args});
   return _result($self->send_request($request));
@@ -179,6 +191,14 @@
 
 Calls a tool on the MCP server with the specified name and arguments, 
returning the result.
 
+=head2 delete_session
+
+  my $bool = $client->delete_session;
+
+Send a C<DELETE> request to terminate the current session on the MCP server, 
and clear the local
+L</"session_id">. Returns true on success, or C<undef> if no session is 
active. The server only honors this when it
+was configured with C<< streaming => 1 >>.
+
 =head2 get_prompt
 
   my $result = $client->get_prompt('prompt_name');
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Primitive.pm 
new/MCP-0.10/lib/MCP/Primitive.pm
--- old/MCP-0.08/lib/MCP/Primitive.pm   1970-01-01 01:00:00.000000000 +0100
+++ new/MCP-0.10/lib/MCP/Primitive.pm   2026-05-06 22:55:47.000000000 +0200
@@ -0,0 +1,46 @@
+package MCP::Primitive;
+use Mojo::Base -base, -signatures;
+
+use MCP::Server::Context;
+
+sub context ($self) { $self->{context} || MCP::Server::Context->new }
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+MCP::Primitive - Primitive base class
+
+=head1 SYNOPSIS
+
+  package MyMCPPrimitive;
+  use Mojo::Base 'MCP::Primitive';
+
+  1;
+
+=head1 DESCRIPTION
+
+L<MCP::Primitive> is a base class for MCP (Model Context Protocol) primitives 
such as L<MCP::Tool>, L<MCP::Prompt>,
+and L<MCP::Resource>.
+
+=head1 METHODS
+
+L<MCP::Primitive> inherits all methods from L<Mojo::Base> and implements the 
following new ones.
+
+=head2 context
+
+  my $context = $primitive->context;
+
+Returns the L<MCP::Server::Context> for the current request. Capture this 
before an async boundary to keep using
+its notification methods from later callbacks.
+
+  # Get controller for requests using the HTTP transport
+  my $c = $primitive->context->controller;
+
+=head1 SEE ALSO
+
+L<MCP>, L<https://mojolicious.org>, L<https://modelcontextprotocol.io>.
+
+=cut
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Prompt.pm 
new/MCP-0.10/lib/MCP/Prompt.pm
--- old/MCP-0.08/lib/MCP/Prompt.pm      2026-01-16 17:04:58.000000000 +0100
+++ new/MCP-0.10/lib/MCP/Prompt.pm      2026-05-06 21:53:53.000000000 +0200
@@ -1,5 +1,5 @@
 package MCP::Prompt;
-use Mojo::Base -base, -signatures;
+use Mojo::Base 'MCP::Primitive', -signatures;
 
 use Scalar::Util qw(blessed);
 
@@ -15,8 +15,6 @@
   return $self->_type_check($result);
 }
 
-sub context ($self) { $self->{context} || {} }
-
 sub text_prompt ($self, $text, $role = 'user', $description = undef) {
   my $result = {messages => [{role => $role, content => {type => 'text', text 
=> "$text"}}]};
   $result->{description} = $description if defined $description;
@@ -88,7 +86,7 @@
 
 =head1 METHODS
 
-L<MCP::Prompt> inherits all methods from L<Mojo::Base> and implements the 
following new ones.
+L<MCP::Prompt> inherits all methods from L<MCP::Primitive> and implements the 
following new ones.
 
 =head2 call
 
@@ -97,15 +95,6 @@
 Calls the prompt with the given arguments and context, returning a result. The 
result can be a promise or a direct
 value.
 
-=head2 context
-
-  my $context = $prompt->context;
-
-Returns the context in which the prompt is executed.
-
-  # Get controller for requests using the HTTP transport
-  my $c = $prompt->context->{controller};
-
 =head2 text_prompt
 
   my $result = $prompt->text_prompt('Some text');
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Resource.pm 
new/MCP-0.10/lib/MCP/Resource.pm
--- old/MCP-0.08/lib/MCP/Resource.pm    2025-12-05 14:06:47.000000000 +0100
+++ new/MCP-0.10/lib/MCP/Resource.pm    2026-05-06 21:53:53.000000000 +0200
@@ -1,5 +1,5 @@
 package MCP::Resource;
-use Mojo::Base -base, -signatures;
+use Mojo::Base 'MCP::Primitive', -signatures;
 
 use Mojo::Util   qw(b64_encode);
 use Scalar::Util qw(blessed);
@@ -22,8 +22,6 @@
   return $self->_type_check($result);
 }
 
-sub context ($self) { $self->{context} || {} }
-
 sub text_resource ($self, $text) {
   my $result = {contents => [{uri => $self->uri, mimeType => $self->mime_type, 
text => $text}]};
   return $result;
@@ -93,7 +91,7 @@
 
 =head1 METHODS
 
-L<MCP::Resource> inherits all methods from L<Mojo::Base> and implements the 
following new ones.
+L<MCP::Resource> inherits all methods from L<MCP::Primitive> and implements 
the following new ones.
 
 =head2 binary_resource
 
@@ -107,15 +105,6 @@
 
 Calls the resource with context, returning a result. The result can be a 
promise or a direct value.
 
-=head2 context
-
-  my $context = $resource->context;
-
-Returns the context in which the resouce is executed.
-
-  # Get controller for requests using the HTTP transport
-  my $c = $resource->context->{controller};
-
 =head2 text_resource
 
   my $result = $resource->text_resource('Some text');
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Server/Context.pm 
new/MCP-0.10/lib/MCP/Server/Context.pm
--- old/MCP-0.08/lib/MCP/Server/Context.pm      1970-01-01 01:00:00.000000000 
+0100
+++ new/MCP-0.10/lib/MCP/Server/Context.pm      2026-05-06 22:55:47.000000000 
+0200
@@ -0,0 +1,95 @@
+package MCP::Server::Context;
+use Mojo::Base -base, -signatures;
+
+has [qw(controller progress_token session_id transport)];
+
+sub notify ($self, $method, $params = {}) {
+  return undef unless my $transport = $self->transport;
+  return $transport->notify($self->session_id, $method, $params);
+}
+
+sub notify_progress ($self, $progress, $total = undef, $message = undef) {
+  return undef unless defined(my $token = $self->progress_token);
+  my $params = {progressToken => $token, progress => $progress};
+  $params->{total}   = $total   if defined $total;
+  $params->{message} = $message if defined $message;
+  return $self->notify('notifications/progress', $params);
+}
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+MCP::Server::Context - Request context container
+
+=head1 SYNOPSIS
+
+  use MCP::Server::Context;
+
+  my $context = MCP::Server::Context->new;
+  $context->notify_progress(1, 2, 'halfway');
+
+=head1 DESCRIPTION
+
+L<MCP::Server::Context> is a container for per-invocation request context.
+
+=head1 ATTRIBUTES
+
+L<MCP::Server::Context> implements the following attributes.
+
+=head2 controller
+
+  my $c    = $context->controller;
+  $context = $context->controller(Mojolicious::Controller->new);
+
+The L<Mojolicious::Controller> serving the current request, when the HTTP 
transport is in use.
+
+=head2 progress_token
+
+  my $token = $context->progress_token;
+  $context  = $context->progress_token('tok-1');
+
+The progress token provided by the client in C<_meta.progressToken>, or 
C<undef> if none was sent.
+
+=head2 session_id
+
+  my $id   = $context->session_id;
+  $context = $context->session_id('12345');
+
+Identifier of the session this request belongs to.
+
+=head2 transport
+
+  my $transport = $context->transport;
+  $context      = $context->transport(MCP::Server::Transport::HTTP->new);
+
+The transport handling the current request.
+
+=head1 METHODS
+
+L<MCP::Server::Context> inherits all methods from L<Mojo::Base> and implements 
the following new ones.
+
+=head2 notify
+
+  my $bool = $context->notify($method);
+  my $bool = $context->notify($method, {foo => 'bar'});
+
+Send a JSON-RPC notification to the client associated with the current 
request. Returns true on success, or
+C<undef> if no notification could be delivered.
+
+=head2 notify_progress
+
+  my $bool = $context->notify_progress($progress);
+  my $bool = $context->notify_progress($progress, $total);
+  my $bool = $context->notify_progress($progress, $total, $message);
+
+Send a C<notifications/progress> JSON-RPC notification for the progress token 
associated with the current request.
+Returns true on success, or C<undef> if no progress token was provided by the 
client.
+
+=head1 SEE ALSO
+
+L<MCP>, L<https://mojolicious.org>, L<https://modelcontextprotocol.io>.
+
+=cut
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Server/Session.pm 
new/MCP-0.10/lib/MCP/Server/Session.pm
--- old/MCP-0.08/lib/MCP/Server/Session.pm      1970-01-01 01:00:00.000000000 
+0100
+++ new/MCP-0.10/lib/MCP/Server/Session.pm      2026-05-06 21:53:53.000000000 
+0200
@@ -0,0 +1,72 @@
+package MCP::Server::Session;
+use Mojo::Base -base, -signatures;
+
+has [qw(id stream)];
+has last_used => sub {time};
+
+sub touch ($self) {
+  $self->last_used(time);
+  return $self;
+}
+
+1;
+
+=encoding utf8
+
+=head1 NAME
+
+MCP::Server::Session - Session container
+
+=head1 SYNOPSIS
+
+  use MCP::Server::Session;
+
+  my $session = MCP::Server::Session->new(id => '12345');
+  $session->touch;
+
+=head1 DESCRIPTION
+
+L<MCP::Server::Session> is a container for per-session state.
+
+=head1 ATTRIBUTES
+
+L<MCP::Server::Session> implements the following attributes.
+
+=head2 id
+
+  my $id   = $session->id;
+  $session = $session->id('12345');
+
+The session identifier.
+
+=head2 last_used
+
+  my $time = $session->last_used;
+  $session = $session->last_used(time);
+
+Epoch seconds of the last activity on this session, defaults to the time the 
session was created. Updated by
+L</"touch">.
+
+=head2 stream
+
+  my $stream = $session->stream;
+  $session   = $session->stream(Mojolicious::Controller->new);
+
+The L<Mojolicious::Controller> currently serving the server-to-client SSE 
stream for this session, or C<undef> if
+no stream is open.
+
+=head1 METHODS
+
+L<MCP::Server::Session> inherits all methods from L<Mojo::Base> and implements 
the following new ones.
+
+=head2 touch
+
+  $session = $session->touch;
+
+Set L</"last_used"> to the current time.
+
+=head1 SEE ALSO
+
+L<MCP>, L<https://mojolicious.org>, L<https://modelcontextprotocol.io>.
+
+=cut
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Server/Transport/HTTP.pm 
new/MCP-0.10/lib/MCP/Server/Transport/HTTP.pm
--- old/MCP-0.08/lib/MCP/Server/Transport/HTTP.pm       2025-08-01 
18:01:33.000000000 +0200
+++ new/MCP-0.10/lib/MCP/Server/Transport/HTTP.pm       2026-05-06 
22:55:47.000000000 +0200
@@ -1,19 +1,48 @@
 package MCP::Server::Transport::HTTP;
 use Mojo::Base 'MCP::Server::Transport', -signatures;
 
-use Crypt::Misc  qw(random_v4uuid);
+use Crypt::Misc qw(random_v4uuid);
+use MCP::Server::Context;
+use MCP::Server::Session;
+use Mojo::IOLoop;
 use Mojo::JSON   qw(to_json true);
 use Mojo::Util   qw(dumper);
-use Scalar::Util qw(blessed);
+use Scalar::Util qw(blessed weaken);
 
 use constant DEBUG => $ENV{MCP_DEBUG} || 0;
 
+has heartbeat       => 30;
+has session_timeout => 3600;
+has sessions        => sub { {} };
+has streaming       => 0;
+
+sub notifications ($self) { $self->streaming ? 1 : 0 }
+
 sub handle_request ($self, $c) {
   my $method = $c->req->method;
-  return $self->_handle_post($c) if $method eq 'POST';
+  return $self->_handle_post($c)   if $method eq 'POST';
+  return $self->_handle_get($c)    if $method eq 'GET'    && $self->streaming;
+  return $self->_handle_delete($c) if $method eq 'DELETE' && $self->streaming;
   return $c->render(json => {error => 'Method not allowed'}, status => 405);
 }
 
+sub notify ($self, $session_id, $method, $params = {}) {
+  return undef unless my $session = $self->sessions->{$session_id};
+  return undef unless my $stream  = $session->stream;
+  $stream->write_sse({text => to_json({jsonrpc => '2.0', method => $method, 
params => $params})});
+  return 1;
+}
+
+sub notify_all ($self, $method, $params = {}) {
+  return undef unless $self->streaming;
+  my $payload = {text => to_json({jsonrpc => '2.0', method => $method, params 
=> $params})};
+  for my $session (values %{$self->sessions}) {
+    next unless my $stream = $session->stream;
+    $stream->write_sse($payload);
+  }
+  return 1;
+}
+
 sub _extract_session_id ($self, $c) { return 
$c->req->headers->header('Mcp-Session-Id') }
 
 sub _handle ($self, $data, $context) {
@@ -23,9 +52,52 @@
   return $result;
 }
 
+sub _handle_delete ($self, $c) {
+  return $c->render(json => {error => 'Missing session ID'}, status => 400)
+    unless my $session_id = $self->_extract_session_id($c);
+  return $c->render(json => {error => 'Session not found'}, status => 404)
+    unless my $session = delete $self->sessions->{$session_id};
+
+  if (my $stream = $session->stream) { $stream->finish }
+  $c->render(data => '', status => 204);
+}
+
+sub _handle_get ($self, $c) {
+  return $c->render(json => {error => 'Missing session ID'}, status => 400)
+    unless my $session_id = $self->_extract_session_id($c);
+  return $c->render(json => {error => 'Session not found'}, status => 404)
+    unless my $session = $self->sessions->{$session_id};
+  return $c->render(json => {error => 'Stream already open'}, status => 409) 
if $session->stream;
+
+  $c->inactivity_timeout(0);
+  $c->res->headers->header('Mcp-Session-Id' => $session_id);
+  $session->stream($c)->touch;
+  $c->write_sse;
+
+  my $heartbeat_id;
+  if (my $interval = $self->heartbeat) {
+    $heartbeat_id = Mojo::IOLoop->recurring($interval => sub { 
$c->write_sse({comment => 'keepalive'}) });
+  }
+
+  weaken(my $self_weak = $self);
+  $c->on(
+    finish => sub {
+      Mojo::IOLoop->remove($heartbeat_id) if $heartbeat_id;
+      return unless $self_weak;
+      return unless my $session = $self_weak->sessions->{$session_id};
+      return unless ($session->stream // 0) == $c;
+      $session->stream(undef)->touch;
+    }
+  );
+}
+
 sub _handle_initialization ($self, $c, $data) {
   my $session_id = random_v4uuid;
-  my $result     = $self->_handle($data, {});
+  my $result     = $self->_handle($data, MCP::Server::Context->new);
+  if ($self->streaming) {
+    $self->sessions->{$session_id} = MCP::Server::Session->new(id => 
$session_id);
+    $self->_start_sweep;
+  }
   $c->res->headers->header('Mcp-Session-Id' => $session_id);
   $c->render(json => $result, status => 200);
 }
@@ -42,10 +114,15 @@
 
 sub _handle_regular_request ($self, $c, $data, $session_id) {
   return $c->render(json => {error => 'Missing session ID'}, status => 400) 
unless $session_id;
+  if ($self->streaming) {
+    return $c->render(json => {error => 'Session not found'}, status => 404)
+      unless my $session = $self->sessions->{$session_id};
+    $session->touch;
+  }
 
   $c->res->headers->header('Mcp-Session-Id' => $session_id);
-  return $c->render(data => '', status => 202)
-    unless defined(my $result = $self->_handle($data, {session_id => 
$session_id, controller => $c}));
+  my $context = MCP::Server::Context->new(transport => $self, session_id => 
$session_id, controller => $c);
+  return $c->render(data => '', status => 202) unless defined(my $result = 
$self->_handle($data, $context));
 
   # Sync
   return $c->render(json => $result, status => 200) if !blessed($result) || 
!$result->isa('Mojo::Promise');
@@ -56,6 +133,23 @@
   $result->then(sub { $c->write_sse({text => to_json($_[0])})->finish });
 }
 
+sub _start_sweep ($self) {
+  return if $self->{_sweep_id};
+  return unless my $interval = $self->session_timeout;
+  weaken(my $self_weak = $self);
+  $self->{_sweep_id} = Mojo::IOLoop->recurring($interval => sub { 
$self_weak->_sweep if $self_weak });
+}
+
+sub _sweep ($self) {
+  return unless my $timeout = $self->session_timeout;
+  my $cutoff   = time - $timeout;
+  my $sessions = $self->sessions;
+  for my $id (keys %$sessions) {
+    my $session = $sessions->{$id};
+    delete $sessions->{$id} if !$session->stream && $session->last_used < 
$cutoff;
+  }
+}
+
 1;
 
 =encoding utf8
@@ -75,9 +169,51 @@
 L<MCP::Server::Transport::HTTP> is a transport for MCP (Model Context 
Protocol) server that uses HTTP as the
 underlying transport mechanism.
 
+By default only C<POST> requests are handled. When L</"streaming"> is enabled, 
the transport additionally supports
+the server-to-client SSE stream (C<GET>) and explicit session termination 
(C<DELETE>) defined by the Streamable
+HTTP transport. Note that this requires per-process state and is therefore not 
compatible with pre-forking web
+servers.
+
 =head1 ATTRIBUTES
 
-L<MCP::Server::Transport::HTTP> inherits all attributes from 
L<MCP::Server::Transport>.
+L<MCP::Server::Transport::HTTP> inherits all attributes from 
L<MCP::Server::Transport> and implements the following
+new ones.
+
+=head2 heartbeat
+
+  my $seconds = $http->heartbeat;
+  $http       = $http->heartbeat(30);
+
+Interval in seconds at which a keep-alive comment is sent on each open 
server-to-client stream. Defaults to C<30>;
+set to C<0> to disable. Useful when running behind reverse proxies that close 
idle connections. Only used when
+L</"streaming"> is enabled.
+
+=head2 session_timeout
+
+  my $seconds = $http->session_timeout;
+  $http       = $http->session_timeout(3600);
+
+Idle timeout in seconds for sessions without an open server-to-client stream. 
Defaults to C<3600>; set to C<0> to
+disable. A periodic sweep removes sessions whose last activity is older than 
this value, so the effective lifetime
+of an idle session is up to twice the configured timeout. Only used when 
L</"streaming"> is enabled.
+
+=head2 sessions
+
+  my $sessions = $http->sessions;
+  $http        = $http->sessions({});
+
+Per-process registry of active L<MCP::Server::Session> objects, keyed by 
session ID. Only used when L</"streaming">
+is enabled.
+
+=head2 streaming
+
+  my $bool = $http->streaming;
+  $http    = $http->streaming(1);
+
+Enable server-to-client streaming and session lifecycle management. Defaults 
to false. When enabled, the transport
+tracks all sessions in L</"sessions">, accepts C<GET> requests to open a 
long-lived SSE stream the server can push
+notifications to, and accepts C<DELETE> requests to terminate a session. 
Requests for unknown sessions are rejected
+with status C<404>.
 
 =head1 METHODS
 
@@ -90,6 +226,28 @@
 
 Handles an incoming HTTP request.
 
+=head2 notifications
+
+  my $bool = $http->notifications;
+
+True when L</"streaming"> is enabled, false otherwise.
+
+=head2 notify
+
+  my $bool = $http->notify($session_id, $method);
+  my $bool = $http->notify($session_id, $method, {foo => 'bar'});
+
+Send a JSON-RPC notification to the open SSE stream of a session. Returns true 
on success, or C<undef> if the
+session does not exist or has no open stream. Only available when 
L</"streaming"> is enabled.
+
+=head2 notify_all
+
+  my $bool = $http->notify_all($method);
+  my $bool = $http->notify_all($method, {foo => 'bar'});
+
+Send a JSON-RPC notification to the open SSE stream of every active session. 
Returns true on success, or C<undef>
+when L</"streaming"> is disabled.
+
 =head1 SEE ALSO
 
 L<MCP>, L<https://mojolicious.org>, L<https://modelcontextprotocol.io>.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Server/Transport/Stdio.pm 
new/MCP-0.10/lib/MCP/Server/Transport/Stdio.pm
--- old/MCP-0.08/lib/MCP/Server/Transport/Stdio.pm      2025-08-01 
16:24:57.000000000 +0200
+++ new/MCP-0.10/lib/MCP/Server/Transport/Stdio.pm      2026-05-06 
22:55:47.000000000 +0200
@@ -1,6 +1,7 @@
 package MCP::Server::Transport::Stdio;
 use Mojo::Base 'MCP::Server::Transport', -signatures;
 
+use MCP::Server::Context;
 use Mojo::JSON qw(decode_json encode_json);
 use Mojo::Log;
 use Scalar::Util qw(blessed);
@@ -12,7 +13,7 @@
   while (my $input = <>) {
     chomp $input;
     my $request = eval { decode_json($input) };
-    next unless my $response = $server->handle($request, {});
+    next unless my $response = $server->handle($request, 
MCP::Server::Context->new(transport => $self));
 
     if (blessed($response) && $response->isa('Mojo::Promise')) {
       $response->then(sub { _print_response($_[0]) })->wait;
@@ -21,6 +22,13 @@
   }
 }
 
+sub notify ($self, $session_id, $method, $params = {}) {
+  _print_response({jsonrpc => '2.0', method => $method, params => $params});
+  return 1;
+}
+
+sub notify_all ($self, $method, $params = {}) { $self->notify(undef, $method, 
$params) }
+
 sub _print_response ($response) { print encode_json($response) . "\n" }
 
 1;
@@ -58,6 +66,20 @@
 
 Reads requests from standard input and prints responses to standard output.
 
+=head2 notify
+
+  my $bool = $stdio->notify($session_id, $method);
+  my $bool = $stdio->notify($session_id, $method, {foo => 'bar'});
+
+Send a JSON-RPC notification to standard output. The C<$session_id> is ignored.
+
+=head2 notify_all
+
+  my $bool = $stdio->notify_all($method);
+  my $bool = $stdio->notify_all($method, {foo => 'bar'});
+
+Send a JSON-RPC notification to standard output.
+
 =head1 SEE ALSO
 
 L<MCP>, L<https://mojolicious.org>, L<https://modelcontextprotocol.io>.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Server/Transport.pm 
new/MCP-0.10/lib/MCP/Server/Transport.pm
--- old/MCP-0.08/lib/MCP/Server/Transport.pm    2025-08-01 16:24:21.000000000 
+0200
+++ new/MCP-0.10/lib/MCP/Server/Transport.pm    2026-05-06 21:53:53.000000000 
+0200
@@ -3,6 +3,8 @@
 
 has 'server';
 
+sub notifications ($self) {1}
+
 1;
 
 =encoding utf8
@@ -33,6 +35,16 @@
 
 The server instance that this transport is associated with.
 
+=head1 METHODS
+
+L<MCP::Server::Transport> implements the following methods.
+
+=head2 notifications
+
+  my $bool = $transport->notifications;
+
+True when the transport can push server-to-client notifications outside an 
in-flight response.
+
 =head1 SEE ALSO
 
 L<Mojolicious>, L<https://mojolicious.org>, L<https://modelcontextprotocol.io>.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Server.pm 
new/MCP-0.10/lib/MCP/Server.pm
--- old/MCP-0.08/lib/MCP/Server.pm      2026-02-17 13:13:13.000000000 +0100
+++ new/MCP-0.10/lib/MCP/Server.pm      2026-05-06 22:55:47.000000000 +0200
@@ -25,6 +25,9 @@
   # Requests
   if (defined(my $id = $request->{id})) {
 
+    my $token = ($request->{params} // {})->{_meta}{progressToken};
+    $context->progress_token($token) if defined $token;
+
     if ($method eq 'initialize') {
       my $result = $self->_handle_initialize($request->{params} // {});
       return _jsonrpc_response($result, $id);
@@ -62,6 +65,11 @@
   return undef;
 }
 
+sub notify_list_changed ($self, $kind) {
+  return undef unless my $transport = $self->transport;
+  return $transport->notify_all("notifications/$kind/list_changed");
+}
+
 sub prompt ($self, %args) {
   my $prompt = MCP::Prompt->new(%args);
   push @{$self->prompts}, $prompt;
@@ -74,8 +82,8 @@
   return $resource;
 }
 
-sub to_action ($self) {
-  $self->transport(my $http = MCP::Server::Transport::HTTP->new(server => 
$self));
+sub to_action ($self, $options = {}) {
+  $self->transport(my $http = MCP::Server::Transport::HTTP->new(server => 
$self, %$options));
   return sub ($c) { $http->handle_request($c) };
 }
 
@@ -91,9 +99,11 @@
 }
 
 sub _handle_initialize ($self, $params) {
+  my $transport = $self->transport;
+  my $caps      = $transport && $transport->notifications ? {listChanged => 
true} : {};
   return {
     protocolVersion => PROTOCOL_VERSION,
-    capabilities    => {prompts => {}, resources => {}, tools => {}},
+    capabilities    => {prompts => $caps, resources => $caps, tools => $caps},
     serverInfo      => {name    => $self->name, version => $self->version}
   };
 }
@@ -322,6 +332,13 @@
 
 Handle a JSON-RPC request and return a response.
 
+=head2 notify_list_changed
+
+  my $bool = $server->notify_list_changed('tools');
+
+Broadcast a C<notifications/$kind/list_changed> JSON-RPC notification to all 
connected clients. Returns true on
+success, or C<undef> if no notification could be delivered.
+
 =head2 prompt
 
   my $prompt = $server->prompt(
@@ -348,8 +365,11 @@
 =head2 to_action
 
   my $action = $server->to_action;
+  my $action = $server->to_action({streaming => 1});
 
-Convert the server to a L<Mojolicious> action.
+Convert the server to a L<Mojolicious> action. Any options are passed through 
to the constructor of
+L<MCP::Server::Transport::HTTP>; in particular, C<< streaming => 1 >> opts in 
to the server-to-client SSE stream
+and explicit session termination.
 
 =head2 to_stdio
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP/Tool.pm new/MCP-0.10/lib/MCP/Tool.pm
--- old/MCP-0.08/lib/MCP/Tool.pm        2026-02-17 13:13:13.000000000 +0100
+++ new/MCP-0.10/lib/MCP/Tool.pm        2026-05-06 21:53:53.000000000 +0200
@@ -1,5 +1,5 @@
 package MCP::Tool;
-use Mojo::Base -base, -signatures;
+use Mojo::Base 'MCP::Primitive', -signatures;
 
 use JSON::Validator;
 use Mojo::JSON   qw(false to_json true);
@@ -27,8 +27,6 @@
   return $self->_type_check($result);
 }
 
-sub context ($self) { $self->{context} || {} }
-
 sub image_result ($self, $image, $options = {}, $is_error = 0) {
   return {
     content => [{
@@ -146,7 +144,7 @@
 
 =head1 METHODS
 
-L<MCP::Tool> inherits all methods from L<Mojo::Base> and implements the 
following new ones.
+L<MCP::Tool> inherits all methods from L<MCP::Primitive> and implements the 
following new ones.
 
 =head2 audio_result
 
@@ -172,15 +170,6 @@
 
 Calls the tool with the given arguments and context, returning a result. The 
result can be a promise or a direct value.
 
-=head2 context
-
-  my $context = $tool->context;
-
-Returns the context in which the tool is executed.
-
-  # Get controller for requests using the HTTP transport
-  my $c = $tool->context->{controller};
-
 =head2 image_result
 
   my $result = $tool->image_result($bytes, $options, $is_error);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/lib/MCP.pm new/MCP-0.10/lib/MCP.pm
--- old/MCP-0.08/lib/MCP.pm     2026-02-17 13:14:27.000000000 +0100
+++ new/MCP-0.10/lib/MCP.pm     2026-05-06 21:53:53.000000000 +0200
@@ -1,7 +1,7 @@
 package MCP;
 use Mojo::Base -base, -signatures;
 
-our $VERSION = '0.08';
+our $VERSION = '0.10';
 
 1;
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/t/apps/stdio.pl new/MCP-0.10/t/apps/stdio.pl
--- old/MCP-0.08/t/apps/stdio.pl        2025-08-01 00:25:30.000000000 +0200
+++ new/MCP-0.10/t/apps/stdio.pl        2026-05-06 22:55:47.000000000 +0200
@@ -23,5 +23,31 @@
     return $promise;
   }
 );
+$server->tool(
+  name         => 'echo_log',
+  description  => 'Echo the input text and log a notification',
+  input_schema => {type => 'object', properties => {msg => {type => 
'string'}}, required => ['msg']},
+  code         => sub ($tool, $args) {
+    $tool->context->notify('notifications/message', {level => 'info', data => 
$args->{msg}});
+    return "Echo: $args->{msg}";
+  }
+);
+$server->tool(
+  name        => 'reload',
+  description => 'Broadcast a tools list_changed notification',
+  code        => sub ($tool, $args) {
+    $server->notify_list_changed('tools');
+    return 'reloaded';
+  }
+);
+$server->tool(
+  name         => 'echo_progress',
+  description  => 'Echo the input text and report progress',
+  input_schema => {type => 'object', properties => {msg => {type => 
'string'}}, required => ['msg']},
+  code         => sub ($tool, $args) {
+    $tool->context->notify_progress(0.5, 1, 'half');
+    return "Echo: $args->{msg}";
+  }
+);
 
 $server->to_stdio;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/t/lib/MCPStdioTest.pm 
new/MCP-0.10/t/lib/MCPStdioTest.pm
--- old/MCP-0.08/t/lib/MCPStdioTest.pm  2025-07-30 18:31:36.000000000 +0200
+++ new/MCP-0.10/t/lib/MCPStdioTest.pm  2026-05-06 21:53:53.000000000 +0200
@@ -15,15 +15,21 @@
   return 1;
 }
 
+sub read_line ($self) {
+  $self->{timeout}->start(60);
+  pump $self->{run} until $self->{stdout} =~ s/^(.*)\n//;
+  return eval { decode_json($1) };
+}
+
 sub request ($self, $method, $params) {
+  $self->send_request($method, $params);
+  return $self->read_line;
+}
+
+sub send_request ($self, $method, $params) {
   $self->{timeout}->start(60);
   $self->{stdin} .= encode_json($self->client->build_request($method, 
$params)) . "\n";
-
-  my $stdout = $self->{stdout};
-  pump $self->{run} until $self->{stdout} =~ s/^(.*)\n//;
-  my $input = $1;
-  my $res   = eval { decode_json($input) };
-  return $res;
+  return 1;
 }
 
 sub run ($self, @command) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/t/lite_app.t new/MCP-0.10/t/lite_app.t
--- old/MCP-0.08/t/lite_app.t   2026-02-17 13:13:13.000000000 +0100
+++ new/MCP-0.10/t/lite_app.t   2026-05-06 21:53:53.000000000 +0200
@@ -8,6 +8,7 @@
 use Mojo::JSON       qw(from_json true false);
 use MCP::Client;
 use MCP::Constants qw(PROTOCOL_VERSION);
+use MCP::Server;
 
 my $t = Test::Mojo->new(curfile->sibling('apps', 'lite_app.pl'));
 
@@ -15,8 +16,15 @@
   $t->get_ok('/')->status_is(200)->content_like(qr/Hello MCP!/);
 };
 
+subtest 'List changed without streaming' => sub {
+  my $server = MCP::Server->new;
+  $server->to_action;
+  is $server->notify_list_changed('tools'), undef, 'no broadcast without 
streaming';
+};
+
 subtest 'MCP endpoint' => sub {
   $t->get_ok('/mcp')->status_is(405)->content_like(qr/Method not allowed/);
+  $t->delete_ok('/mcp')->status_is(405)->content_like(qr/Method not allowed/);
 
   my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
 
@@ -26,11 +34,14 @@
     is $result->{protocolVersion},     PROTOCOL_VERSION, 'protocol version';
     is $result->{serverInfo}{name},    'PerlServer',     'server name';
     is $result->{serverInfo}{version}, '1.0.0',          'server version';
-    ok $result->{capabilities},            'has capabilities';
-    ok $result->{capabilities}{prompts},   'has prompts capability';
-    ok $result->{capabilities}{resources}, 'has resources capability';
-    ok $result->{capabilities}{tools},     'has tools capability';
-    ok $client->session_id,                'session id set';
+    ok $result->{capabilities},                                 'has 
capabilities';
+    ok $result->{capabilities}{prompts},                        'has prompts 
capability';
+    ok $result->{capabilities}{resources},                      'has resources 
capability';
+    ok $result->{capabilities}{tools},                          'has tools 
capability';
+    ok !exists $result->{capabilities}{tools}{listChanged},     'no 
listChanged for tools';
+    ok !exists $result->{capabilities}{prompts}{listChanged},   'no 
listChanged for prompts';
+    ok !exists $result->{capabilities}{resources}{listChanged}, 'no 
listChanged for resources';
+    ok $client->session_id,                                     'session id 
set';
   };
 
   subtest 'Ping' => sub {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/t/stdio.t new/MCP-0.10/t/stdio.t
--- old/MCP-0.08/t/stdio.t      2025-08-01 00:26:57.000000000 +0200
+++ new/MCP-0.10/t/stdio.t      2026-05-06 22:55:47.000000000 +0200
@@ -8,7 +8,7 @@
 
 use MCP::Constants qw(PROTOCOL_VERSION);
 use Mojo::File     qw(curfile);
-use Mojo::JSON     qw(false);
+use Mojo::JSON     qw(false true);
 use lib curfile->dirname->child('lib')->to_string;
 use MCPStdioTest;
 
@@ -24,6 +24,9 @@
   is $res->{result}{serverInfo}{name},    'PerlServer',     'server name';
   is $res->{result}{serverInfo}{version}, '1.0.0',          'server version';
   ok $res->{result}{capabilities}, 'has capabilities';
+  is $res->{result}{capabilities}{tools}{listChanged},     true, 'tools 
listChanged';
+  is $res->{result}{capabilities}{prompts}{listChanged},   true, 'prompts 
listChanged';
+  is $res->{result}{capabilities}{resources}{listChanged}, true, 'resources 
listChanged';
 
   ok $test->notify('notifications/initialized', {}), 'initialized';
 };
@@ -64,6 +67,49 @@
     'tool call result';
 };
 
+subtest 'Tool call (with notification)' => sub {
+  $test->send_request('tools/call', {name => 'echo_log', arguments => {msg => 
'hi'}});
+  my $notif = $test->read_line;
+  is $notif->{jsonrpc},       '2.0',                   'JSON-RPC version';
+  is $notif->{id},            undef,                   'no request id';
+  is $notif->{method},        'notifications/message', 'notification method';
+  is $notif->{params}{level}, 'info',                  'notification level';
+  is $notif->{params}{data},  'hi',                    'notification payload';
+  my $res = $test->read_line;
+  is $res->{jsonrpc}, '2.0', 'JSON-RPC version';
+  is $res->{id},      6,     'request id';
+  is_deeply $res->{result}, {content => [{text => 'Echo: hi', type => 
'text'}], isError => false}, 'tool call result';
+};
+
+subtest 'Tool call (with broadcast)' => sub {
+  $test->send_request('tools/call', {name => 'reload', arguments => {}});
+  my $notif = $test->read_line;
+  is $notif->{jsonrpc}, '2.0',                              'JSON-RPC version';
+  is $notif->{id},      undef,                              'no request id';
+  is $notif->{method},  'notifications/tools/list_changed', 'notification 
method';
+  my $res = $test->read_line;
+  is $res->{jsonrpc}, '2.0', 'JSON-RPC version';
+  is $res->{id},      7,     'request id';
+  is_deeply $res->{result}, {content => [{text => 'reloaded', type => 
'text'}], isError => false}, 'tool call result';
+};
+
+subtest 'Tool call (with progress)' => sub {
+  $test->send_request('tools/call',
+    {name => 'echo_progress', arguments => {msg => 'hi'}, _meta => 
{progressToken => 'p1'}});
+  my $notif = $test->read_line;
+  is $notif->{jsonrpc},               '2.0',                    'JSON-RPC 
version';
+  is $notif->{id},                    undef,                    'no request 
id';
+  is $notif->{method},                'notifications/progress', 'notification 
method';
+  is $notif->{params}{progressToken}, 'p1',                     'progress 
token echoed';
+  is $notif->{params}{progress},      0.5,                      'progress 
value';
+  is $notif->{params}{total},         1,                        'total value';
+  is $notif->{params}{message},       'half',                   'progress 
message';
+  my $res = $test->read_line;
+  is $res->{jsonrpc}, '2.0', 'JSON-RPC version';
+  is $res->{id},      8,     'request id';
+  is_deeply $res->{result}, {content => [{text => 'Echo: hi', type => 
'text'}], isError => false}, 'tool call result';
+};
+
 ok $test->stop, 'process stopped';
 
 done_testing;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.08/t/streaming.t new/MCP-0.10/t/streaming.t
--- old/MCP-0.08/t/streaming.t  1970-01-01 01:00:00.000000000 +0100
+++ new/MCP-0.10/t/streaming.t  2026-05-06 22:55:47.000000000 +0200
@@ -0,0 +1,321 @@
+use Mojo::Base -strict, -signatures;
+
+use Test::More;
+
+use Mojolicious::Lite;
+use Test::Mojo;
+use Mojo::IOLoop;
+use Mojo::JSON qw(from_json true);
+use Mojo::Promise;
+use MCP::Client;
+use MCP::Server;
+
+my $server = MCP::Server->new;
+
+$server->tool(
+  name => 'push_log',
+  code => sub ($tool, $args) {
+    $tool->context->notify('notifications/message', {level => 'info', data => 
'hello stream'});
+    return 'pushed';
+  }
+);
+$server->tool(
+  name => 'notify_status',
+  code => sub ($tool, $args) {
+    my $sent = $tool->context->notify('notifications/message', {data => 'x'});
+    return $sent ? 'sent' : 'no stream';
+  }
+);
+$server->tool(
+  name => 'progress',
+  code => sub ($tool, $args) {
+    my $sent = $tool->context->notify_progress(1, 2, 'halfway');
+    return $sent ? 'sent' : 'no token';
+  }
+);
+$server->tool(
+  name => 'async_progress',
+  code => sub ($tool, $args) {
+    my $context = $tool->context;
+    my $promise = Mojo::Promise->new;
+    Mojo::IOLoop->timer(
+      0.1 => sub {
+        $context->notify_progress(1, 2, 'late');
+        $promise->resolve('done');
+      }
+    );
+    return $promise;
+  }
+);
+
+any '/mcp' => $server->to_action({streaming => 1, heartbeat => 0, 
session_timeout => 0.5});
+
+my $t = Test::Mojo->new;
+
+subtest 'No session' => sub {
+  $t->get_ok('/mcp')->status_is(400)->json_is('/error' => 'Missing session 
ID');
+  $t->delete_ok('/mcp')->status_is(400)->json_is('/error' => 'Missing session 
ID');
+};
+
+subtest 'Unknown session' => sub {
+  $t->get_ok('/mcp' => {'Mcp-Session-Id' => 'nope'})->status_is(404);
+  $t->delete_ok('/mcp' => {'Mcp-Session-Id' => 'nope'})->status_is(404);
+
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  eval { $client->session_id('nope'); $client->ping };
+  like $@, qr/404 response/, 'POST with unknown session is rejected';
+};
+
+subtest 'List changed' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  my $caps   = $client->initialize_session->{capabilities};
+  is $caps->{tools}{listChanged},     true, 'tools listChanged advertised';
+  is $caps->{prompts}{listChanged},   true, 'prompts listChanged advertised';
+  is $caps->{resources}{listChanged}, true, 'resources listChanged advertised';
+
+  my $got_notification = Mojo::Promise->new;
+  my $msg;
+  my $url = $t->ua->server->url->path('/mcp');
+  my $tx  = $t->ua->build_tx(GET => $url => {Accept => 'text/event-stream', 
'Mcp-Session-Id' => $client->session_id});
+  $tx->res->content->on(
+    sse => sub ($content, $event = undef) {
+      return if $msg;
+      return unless $event && $event->{text} && (my $parsed = eval { 
from_json($event->{text}) });
+      $msg = $parsed;
+      $got_notification->resolve;
+    }
+  );
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+
+  ok $server->notify_list_changed('tools'), 'broadcast attempted';
+  $got_notification->timeout(5)->wait;
+  is $msg->{jsonrpc}, '2.0',                              'JSON-RPC version';
+  is $msg->{method},  'notifications/tools/list_changed', 'notification 
method';
+
+  $client->delete_session;
+};
+
+subtest 'List changed (no streams)' => sub {
+  ok $server->notify_list_changed('prompts'), 'broadcast attempted';
+};
+
+subtest 'Bidirectional flow' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+  ok $client->session_id, 'session id set';
+
+  my $got_notification = Mojo::Promise->new;
+  my $msg;
+  my $url = $t->ua->server->url->path('/mcp');
+  my $tx  = $t->ua->build_tx(GET => $url => {Accept => 'text/event-stream', 
'Mcp-Session-Id' => $client->session_id});
+  $tx->res->content->on(
+    sse => sub ($content, $event = undef) {
+      return if $msg;
+      return unless $event && $event->{text} && (my $parsed = eval { 
from_json($event->{text}) });
+      $msg = $parsed;
+      $got_notification->resolve;
+    }
+  );
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+  is $tx->res->code,                  200,                 'stream open';
+  is $tx->res->headers->content_type, 'text/event-stream', 'right content 
type';
+
+  my $result = $client->call_tool('push_log');
+  is $result->{content}[0]{text}, 'pushed', 'tool call result';
+
+  $got_notification->timeout(5)->wait;
+  is $msg->{jsonrpc},       '2.0',                   'JSON-RPC version';
+  is $msg->{method},        'notifications/message', 'notification method';
+  is $msg->{params}{data},  'hello stream',          'notification payload';
+  is $msg->{params}{level}, 'info',                  'notification level';
+
+  $t->get_ok('/mcp' => {'Mcp-Session-Id' => 
$client->session_id})->status_is(409);
+
+  my $session_id = $client->session_id;
+  ok $client->delete_session, 'session deleted';
+  is $client->session_id, undef, 'session id cleared';
+
+  my $closed = Mojo::Promise->new;
+  $tx->on(finish => sub { $closed->resolve });
+  $closed->timeout(5)->wait unless $tx->is_finished;
+  ok $tx->is_finished, 'stream closed by server';
+
+  $t->get_ok('/mcp' => {'Mcp-Session-Id' => $session_id})->status_is(404);
+  $t->delete_ok('/mcp' => {'Mcp-Session-Id' => $session_id})->status_is(404);
+};
+
+subtest 'Notify (no stream)' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+  my $result = $client->call_tool('notify_status');
+  is $result->{content}[0]{text}, 'no stream', 'notify returns false without 
an open stream';
+  $client->delete_session;
+};
+
+subtest 'Progress notifications' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+
+  my $got_notification = Mojo::Promise->new;
+  my $msg;
+  my $url = $t->ua->server->url->path('/mcp');
+  my $tx  = $t->ua->build_tx(GET => $url => {Accept => 'text/event-stream', 
'Mcp-Session-Id' => $client->session_id});
+  $tx->res->content->on(
+    sse => sub ($content, $event = undef) {
+      return if $msg;
+      return unless $event && $event->{text} && (my $parsed = eval { 
from_json($event->{text}) });
+      $msg = $parsed;
+      $got_notification->resolve;
+    }
+  );
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+
+  my $request
+    = $client->build_request('tools/call', {name => 'progress', arguments => 
{}, _meta => {progressToken => 'tok-1'}});
+  my $response = $client->send_request($request);
+  is $response->{result}{content}[0]{text}, 'sent', 'tool call result';
+
+  $got_notification->timeout(5)->wait;
+  is $msg->{jsonrpc},               '2.0',                    'JSON-RPC 
version';
+  is $msg->{method},                'notifications/progress', 'notification 
method';
+  is $msg->{params}{progressToken}, 'tok-1',                  'progress token 
echoed';
+  is $msg->{params}{progress},      1,                        'progress value';
+  is $msg->{params}{total},         2,                        'total value';
+  is $msg->{params}{message},       'halfway',                'progress 
message';
+
+  $client->delete_session;
+};
+
+subtest 'Progress notifications (async)' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+
+  my $got_notification = Mojo::Promise->new;
+  my $msg;
+  my $url = $t->ua->server->url->path('/mcp');
+  my $tx  = $t->ua->build_tx(GET => $url => {Accept => 'text/event-stream', 
'Mcp-Session-Id' => $client->session_id});
+  $tx->res->content->on(
+    sse => sub ($content, $event = undef) {
+      return if $msg;
+      return unless $event && $event->{text} && (my $parsed = eval { 
from_json($event->{text}) });
+      $msg = $parsed;
+      $got_notification->resolve;
+    }
+  );
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+
+  my $request = $client->build_request('tools/call',
+    {name => 'async_progress', arguments => {}, _meta => {progressToken => 
'tok-2'}});
+  my $response = $client->send_request($request);
+  is $response->{result}{content}[0]{text}, 'done', 'tool call result';
+
+  $got_notification->timeout(5)->wait;
+  is $msg->{jsonrpc},               '2.0',                    'JSON-RPC 
version';
+  is $msg->{method},                'notifications/progress', 'notification 
method';
+  is $msg->{params}{progressToken}, 'tok-2',                  'progress token 
echoed';
+  is $msg->{params}{progress},      1,                        'progress value';
+  is $msg->{params}{total},         2,                        'total value';
+  is $msg->{params}{message},       'late',                   'progress 
message';
+
+  $client->delete_session;
+};
+
+subtest 'Progress (no token)' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+  my $result = $client->call_tool('progress');
+  is $result->{content}[0]{text}, 'no token', 'notify_progress returns false 
without a token';
+  $client->delete_session;
+};
+
+subtest 'Delete (no stream)' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+  my $session_id = $client->session_id;
+  ok $client->delete_session, 'session deleted';
+  $t->get_ok('/mcp' => {'Mcp-Session-Id' => $session_id})->status_is(404);
+};
+
+subtest 'Stream cleanup on disconnect' => sub {
+  my $transport = $server->transport;
+  my $client    = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+  my $session_id = $client->session_id;
+
+  my $url = $t->ua->server->url->path('/mcp');
+  my $tx  = $t->ua->build_tx(GET => $url => {Accept => 'text/event-stream', 
'Mcp-Session-Id' => $session_id});
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+  ok $transport->sessions->{$session_id}->stream, 'stream registered';
+
+  my $closed = Mojo::Promise->new;
+  $tx->on(finish => sub { $closed->resolve });
+  $transport->sessions->{$session_id}->stream->finish;
+  $closed->timeout(5)->wait;
+  ok !$transport->sessions->{$session_id}->stream, 'stream cleared on finish';
+
+  $client->delete_session;
+};
+
+subtest 'Heartbeat' => sub {
+  my $transport = $server->transport;
+  $transport->heartbeat(1);
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $client->initialize_session;
+  my $session_id = $client->session_id;
+
+  my $url = $t->ua->server->url->path('/mcp');
+  my $tx  = $t->ua->build_tx(GET => $url => {Accept => 'text/event-stream', 
'Mcp-Session-Id' => $session_id});
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+  is $tx->res->code, 200, 'stream open';
+
+  # SSE parser strips comments
+  my $bytes = '';
+  Mojo::IOLoop->stream($tx->connection)->on(read => sub ($stream, $chunk) { 
$bytes .= $chunk });
+
+  my $deadline = Mojo::Promise->new;
+  Mojo::IOLoop->timer(1.5 => sub { $deadline->resolve });
+  $deadline->wait;
+  like $bytes, qr/: keepalive/, 'heartbeat sent';
+
+  $transport->heartbeat(0);
+  $client->delete_session;
+};
+
+subtest 'Session expiration' => sub {
+  my $sessions = $server->transport->sessions;
+
+  my $idle = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $idle->initialize_session;
+  my $idle_id = $idle->session_id;
+
+  my $open = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'));
+  $open->initialize_session;
+  my $open_id = $open->session_id;
+  my $url     = $t->ua->server->url->path('/mcp');
+  my $tx      = $t->ua->build_tx(GET => $url => {Accept => 
'text/event-stream', 'Mcp-Session-Id' => $open_id});
+  $t->ua->start_p($tx)->catch(sub { });
+  Mojo::IOLoop->one_tick until $tx->res->code || $tx->error;
+
+  ok exists $sessions->{$idle_id}, 'idle session registered';
+  ok exists $sessions->{$open_id}, 'streaming session registered';
+
+  my $tick = Mojo::Promise->new;
+  Mojo::IOLoop->timer(1.5 => sub { $tick->resolve });
+  $tick->wait;
+
+  ok !exists $sessions->{$idle_id}, 'idle session swept';
+  ok exists $sessions->{$open_id},  'streaming session survives sweep';
+
+  $open->delete_session;
+
+  eval { $idle->ping };
+  like $@, qr/404 response/, 'POST for swept session is rejected';
+};
+
+done_testing;

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.helyn7/_old  2026-05-18 17:48:48.010244961 +0200
+++ /var/tmp/diff_new_pack.helyn7/_new  2026-05-18 17:48:48.014245127 +0200
@@ -1,6 +1,6 @@
-mtime: 1771395409
-commit: 5fce262571f96f3e649b7c5fce0471e36b7911ce4d8f0b70b46e7f5dde1753a6
-url: https://src.opensuse.org/perl/perl-MCP.git
-revision: 5fce262571f96f3e649b7c5fce0471e36b7911ce4d8f0b70b46e7f5dde1753a6
+mtime: 1778141034
+commit: 2743c0c65b98fe23eefe3241e65f3926ea2b56fd6924cd9044f09a4d0c0d5a98
+url: https://src.opensuse.org/perl/perl-MCP
+revision: 2743c0c65b98fe23eefe3241e65f3926ea2b56fd6924cd9044f09a4d0c0d5a98
 projectscmsync: https://src.opensuse.org/perl/_ObsPrj
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-05-07 10:03:54.000000000 +0200
@@ -0,0 +1 @@
+.osc

Reply via email to