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-06-29 17:30:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/perl-MCP (Old)
 and      /work/SRC/openSUSE:Factory/.perl-MCP.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "perl-MCP"

Mon Jun 29 17:30:23 2026 rev:7 rq:1362461 version:0.120.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/perl-MCP/perl-MCP.changes        2026-05-18 
17:48:47.234212894 +0200
+++ /work/SRC/openSUSE:Factory/.perl-MCP.new.11887/perl-MCP.changes     
2026-06-29 17:31:15.447868871 +0200
@@ -1,0 +2,27 @@
+Mon Jun 29 10:40:19 UTC 2026 - Tina Müller <[email protected]>
+
+- Downgrade to 0.12 again to avoid the new (failing) dependency for now
+
+-------------------------------------------------------------------
+Mon Jun 22 10:52:47 UTC 2026 - Tina Müller <[email protected]>
+
+- updated to 0.130.0 (0.13)
+   see /usr/share/doc/packages/perl-MCP/Changes
+
+  0.13  2026-06-22
+    - Switched from JSON::Valdiator to JSON::Schema::Tiny for better 
portability.
+
+  0.12  2026-06-19
+    - Fixed bug in MCP::Server::Transport::Stdio where the server could 
disconnect when reading from Windows pipes,
+      such as when spawned by Claude Desktop.
+
+  0.11  2026-06-19
+    - Added OAuth scope support, so MCP servers can act as OAuth 2.0 resource 
servers.
+    - Added scopes attribute to MCP::Primitive, and therefore to MCP::Tool, 
MCP::Prompt, and MCP::Resource.
+    - Added scopes and insufficient_scope attributes, and a has_scope method, 
to MCP::Server::Context.
+    - Added auth and metadata_url attributes to MCP::Server::Transport::HTTP.
+    - Added headers attribute to MCP::Client.
+    - Added oauth_metadata method to MCP::Server.
+    - Added INSUFFICIENT_SCOPE constant to MCP::Constants.
+
+-------------------------------------------------------------------

Old:
----
  MCP-0.10.tar.gz

New:
----
  MCP-0.12.tar.gz

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

Other differences:
------------------
++++++ perl-MCP.spec ++++++
--- /var/tmp/diff_new_pack.FeKFaY/_old  2026-06-29 17:31:16.111891541 +0200
+++ /var/tmp/diff_new_pack.FeKFaY/_new  2026-06-29 17:31:16.111891541 +0200
@@ -18,10 +18,10 @@
 
 %define cpan_name MCP
 Name:           perl-MCP
-Version:        0.100.0
+Version:        0.120.0
 Release:        0
-# 0.10 -> normalize -> 0.100.0
-%define cpan_version 0.10
+# 0.12 -> normalize -> 0.120.0
+%define cpan_version 0.12
 License:        MIT
 Summary:        Connect Perl with AI using MCP (Model Context Protocol)
 URL:            https://metacpan.org/release/%{cpan_name}
@@ -85,7 +85,8 @@
       app->start;
 
     Authentication can be added by the web application, just like for any
-    other route. To allow for MCP applications to scale with prefork web
+    other route. OAuth scopes can be enforced per tool, prompt and
+    resource. To allow for MCP applications to scale with prefork web
     servers, server to client streaming is currentlly avoided when
     possible.
 

++++++ MCP-0.10.tar.gz -> MCP-0.12.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/Changes new/MCP-0.12/Changes
--- old/MCP-0.10/Changes        2026-05-06 22:55:47.000000000 +0200
+++ new/MCP-0.12/Changes        2026-06-19 20:16:48.000000000 +0200
@@ -1,4 +1,17 @@
 
+0.12  2026-06-19
+  - Fixed bug in MCP::Server::Transport::Stdio where the server could 
disconnect when reading from Windows pipes,
+    such as when spawned by Claude Desktop.
+
+0.11  2026-06-19
+  - Added OAuth scope support, so MCP servers can act as OAuth 2.0 resource 
servers.
+  - Added scopes attribute to MCP::Primitive, and therefore to MCP::Tool, 
MCP::Prompt, and MCP::Resource.
+  - Added scopes and insufficient_scope attributes, and a has_scope method, to 
MCP::Server::Context.
+  - Added auth and metadata_url attributes to MCP::Server::Transport::HTTP.
+  - Added headers attribute to MCP::Client.
+  - Added oauth_metadata method to MCP::Server.
+  - Added INSUFFICIENT_SCOPE constant to MCP::Constants.
+
 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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/MANIFEST new/MCP-0.12/MANIFEST
--- old/MCP-0.10/MANIFEST       2026-05-06 22:58:51.000000000 +0200
+++ new/MCP-0.12/MANIFEST       2026-06-19 20:17:23.000000000 +0200
@@ -24,6 +24,7 @@
 t/apps/lite_app.pl
 t/apps/mojolicious.png
 t/apps/stdio.pl
+t/auth_scopes_app.t
 t/lib/MCPStdioTest.pm
 t/lite_app.t
 t/pod.t
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/META.json new/MCP-0.12/META.json
--- old/MCP-0.10/META.json      2026-05-06 22:58:51.000000000 +0200
+++ new/MCP-0.12/META.json      2026-06-19 20:17:22.000000000 +0200
@@ -61,6 +61,6 @@
          "web" : "https://web.libera.chat/#mojo";
       }
    },
-   "version" : "0.10",
+   "version" : "0.12",
    "x_serialization_backend" : "JSON::PP version 4.16"
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/META.yml new/MCP-0.12/META.yml
--- old/MCP-0.10/META.yml       2026-05-06 22:58:51.000000000 +0200
+++ new/MCP-0.12/META.yml       2026-06-19 20:17:22.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.10'
+version: '0.12'
 x_serialization_backend: 'CPAN::Meta::YAML version 0.020'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/README.md new/MCP-0.12/README.md
--- old/MCP-0.10/README.md      2026-05-06 22:55:47.000000000 +0200
+++ new/MCP-0.12/README.md      2026-06-19 20:01:34.000000000 +0200
@@ -1,7 +1,7 @@
 
 # MCP Perl SDK
 
- 
[![](https://github.com/mojolicious/mojo-mcp/workflows/linux/badge.svg)](https://github.com/mojolicious/mojo-mcp/actions)
 
[![](https://github.com/mojolicious/mojo-mcp/workflows/macos/badge.svg)](https://github.com/mojolicious/mojo-mcp/actions)
+ 
[![](https://github.com/mojolicious/mojo-mcp/workflows/linux/badge.svg)](https://github.com/mojolicious/mojo-mcp/actions)
 
[![](https://github.com/mojolicious/mojo-mcp/workflows/macos/badge.svg)](https://github.com/mojolicious/mojo-mcp/actions)
 
[![](https://github.com/mojolicious/mojo-mcp/workflows/windows/badge.svg)](https://github.com/mojolicious/mojo-mcp/actions)
 
   [Model Context Protocol](https://modelcontextprotocol.io/) support for 
[Perl](https://perl.org) and the
   [Mojolicious](https://mojolicious.org) real-time web framework.
@@ -15,6 +15,7 @@
   * Streamable HTTP and Stdio transports
   * Notifications for list changes (tools, prompts, resources)
   * Progress tracking for long-running operations
+  * OAuth scopes for tools, prompts and resources
   * Scalable with pre-forking web server and async tools using promises
   * HTTP client for testing
   * Can be embedded in Mojolicious web apps
@@ -51,7 +52,8 @@
 app->start;
 ```
 
-Authentication can be added by the web application, just like for any other 
route.
+Authentication can be added by the web application, just like for any other 
route. OAuth scopes can be enforced per
+tool, prompt and resource.
 
 ## Server-to-Client Streaming
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Client.pm 
new/MCP-0.12/lib/MCP/Client.pm
--- old/MCP-0.10/lib/MCP/Client.pm      2026-05-06 21:53:53.000000000 +0200
+++ new/MCP-0.12/lib/MCP/Client.pm      2026-06-19 16:05:31.000000000 +0200
@@ -7,7 +7,8 @@
 use Mojo::UserAgent;
 use Scalar::Util qw(weaken);
 
-has name => 'PerlClient';
+has headers => sub { {} };
+has name    => 'PerlClient';
 has 'session_id';
 has ua      => sub { Mojo::UserAgent->new };
 has url     => sub {'http://localhost:3000/mcp'};
@@ -30,7 +31,7 @@
 
 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});
+  my $tx = $self->ua->build_tx(DELETE => $self->url => {%{$self->headers}, 
'Mcp-Session-Id' => $session_id});
   $tx = $self->ua->start($tx);
   if (my $err = $tx->error) {
     croak "$err->{code} response: $err->{message}" if $err->{code};
@@ -69,7 +70,8 @@
 }
 
 sub send_request ($self, $request) {
-  my $headers = {Accept => 'application/json, text/event-stream', 
'Content-Type' => 'application/json'};
+  my $headers
+    = {%{$self->headers}, Accept => 'application/json, text/event-stream', 
'Content-Type' => 'application/json'};
   if (my $session_id = $self->session_id) { $headers->{'Mcp-Session-Id'} = 
$session_id }
   my $ua = $self->ua;
   my $tx = $ua->build_tx(POST => $self->url => $headers => json => $request);
@@ -133,6 +135,14 @@
 
 L<MCP::Client> inherits all attributes from L<Mojo::Base> and implements the 
following new ones.
 
+=head2 headers
+
+  my $headers = $client->headers;
+  $client     = $client->headers({Authorization => 'Bearer abc123'});
+
+Extra HTTP headers to send with every request, as a hash reference. Useful for 
passing an C<Authorization> header to
+an MCP server that requires OAuth bearer authentication. Defaults to an empty 
hash reference.
+
 =head2 name
 
   my $name = $client->name;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Constants.pm 
new/MCP-0.12/lib/MCP/Constants.pm
--- old/MCP-0.10/lib/MCP/Constants.pm   2025-12-04 16:10:03.000000000 +0100
+++ new/MCP-0.12/lib/MCP/Constants.pm   2026-06-19 16:05:31.000000000 +0200
@@ -2,6 +2,7 @@
 use Mojo::Base 'Exporter';
 
 use constant {
+  INSUFFICIENT_SCOPE => -32003,
   INVALID_PARAMS     => -32602,
   INVALID_REQUEST    => -32600,
   METHOD_NOT_FOUND   => -32601,
@@ -10,7 +11,8 @@
   RESOURCE_NOT_FOUND => -32002
 };
 
-our @EXPORT_OK = qw(INVALID_PARAMS INVALID_REQUEST METHOD_NOT_FOUND 
PARSE_ERROR PROTOCOL_VERSION RESOURCE_NOT_FOUND);
+our @EXPORT_OK
+  = qw(INSUFFICIENT_SCOPE INVALID_PARAMS INVALID_REQUEST METHOD_NOT_FOUND 
PARSE_ERROR PROTOCOL_VERSION RESOURCE_NOT_FOUND);
 
 1;
 
@@ -32,6 +34,10 @@
 
 L<MCP::Constants> exports the following constants.
 
+=head2 INSUFFICIENT_SCOPE
+
+The error code for a request whose access token lacks a required OAuth scope.
+
 =head2 INVALID_PARAMS
 
 The error code for invalid parameters.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Primitive.pm 
new/MCP-0.12/lib/MCP/Primitive.pm
--- old/MCP-0.10/lib/MCP/Primitive.pm   2026-05-06 22:55:47.000000000 +0200
+++ new/MCP-0.12/lib/MCP/Primitive.pm   2026-06-19 16:05:31.000000000 +0200
@@ -3,6 +3,8 @@
 
 use MCP::Server::Context;
 
+has scopes => sub { [] };
+
 sub context ($self) { $self->{context} || MCP::Server::Context->new }
 
 1;
@@ -25,6 +27,20 @@
 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 ATTRIBUTES
+
+L<MCP::Primitive> implements the following attributes.
+
+=head2 scopes
+
+  my $scopes = $primitive->scopes;
+  $primitive = $primitive->scopes(['mcp:read', 'mcp:write']);
+
+OAuth scopes required to list or call this primitive, as an array reference; 
all of them must be granted. This is a
+local authorization policy layered on the HTTP transport's 
L<MCP::Server::Transport::HTTP/"auth"> hook, not wire-level
+MCP metadata, and is only enforced for requests that supply scopes (so it has 
no effect over stdio). Defaults to no
+required scopes.
+
 =head1 METHODS
 
 L<MCP::Primitive> inherits all methods from L<Mojo::Base> and implements the 
following new ones.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Server/Context.pm 
new/MCP-0.12/lib/MCP/Server/Context.pm
--- old/MCP-0.10/lib/MCP/Server/Context.pm      2026-05-06 22:55:47.000000000 
+0200
+++ new/MCP-0.12/lib/MCP/Server/Context.pm      2026-06-19 16:05:31.000000000 
+0200
@@ -1,7 +1,14 @@
 package MCP::Server::Context;
 use Mojo::Base -base, -signatures;
 
-has [qw(controller progress_token session_id transport)];
+has [qw(controller insufficient_scope progress_token scopes session_id 
transport)];
+
+sub has_scope ($self, @needed) {
+  return 1 unless defined(my $scopes = $self->scopes);
+  my %granted = map { $_ => 1 } @$scopes;
+  for my $scope (@needed) { return 0 unless $granted{$scope} }
+  return 1;
+}
 
 sub notify ($self, $method, $params = {}) {
   return undef unless my $transport = $self->transport;
@@ -46,6 +53,14 @@
 
 The L<Mojolicious::Controller> serving the current request, when the HTTP 
transport is in use.
 
+=head2 insufficient_scope
+
+  my $needed = $context->insufficient_scope;
+  $context   = $context->insufficient_scope(['mcp:write']);
+
+Array reference of scopes a denied request was missing, set by the server so 
the HTTP transport can emit an
+C<insufficient_scope> challenge. C<undef> when no scope check failed.
+
 =head2 progress_token
 
   my $token = $context->progress_token;
@@ -60,6 +75,15 @@
 
 Identifier of the session this request belongs to.
 
+=head2 scopes
+
+  my $scopes = $context->scopes;
+  $context   = $context->scopes(['mcp:read', 'mcp:write']);
+
+OAuth scopes granted to the current request, as an array reference, populated 
from the C<auth> hook of the HTTP
+transport. C<undef> (the default) imposes no scope restriction, so scopes are 
only enforced for authenticated
+requests that provide them.
+
 =head2 transport
 
   my $transport = $context->transport;
@@ -71,6 +95,13 @@
 
 L<MCP::Server::Context> inherits all methods from L<Mojo::Base> and implements 
the following new ones.
 
+=head2 has_scope
+
+  my $bool = $context->has_scope('mcp:write');
+  my $bool = $context->has_scope('mcp:read', 'mcp:write');
+
+Returns true if every given scope is present in L</"scopes">, or if 
L</"scopes"> is C<undef> (no restriction).
+
 =head2 notify
 
   my $bool = $context->notify($method);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Server/Transport/HTTP.pm 
new/MCP-0.12/lib/MCP/Server/Transport/HTTP.pm
--- old/MCP-0.10/lib/MCP/Server/Transport/HTTP.pm       2026-05-06 
22:55:47.000000000 +0200
+++ new/MCP-0.12/lib/MCP/Server/Transport/HTTP.pm       2026-06-19 
16:05:31.000000000 +0200
@@ -11,7 +11,9 @@
 
 use constant DEBUG => $ENV{MCP_DEBUG} || 0;
 
-has heartbeat       => 30;
+has 'auth';
+has heartbeat => 30;
+has 'metadata_url';
 has session_timeout => 3600;
 has sessions        => sub { {} };
 has streaming       => 0;
@@ -19,6 +21,11 @@
 sub notifications ($self) { $self->streaming ? 1 : 0 }
 
 sub handle_request ($self, $c) {
+  if (my $auth = $self->auth) {
+    return $self->_unauthorized($c) unless my $info = $auth->($c);
+    $c->stash('mcp.auth' => $info);
+  }
+
   my $method = $c->req->method;
   return $self->_handle_post($c)   if $method eq 'POST';
   return $self->_handle_get($c)    if $method eq 'GET'    && $self->streaming;
@@ -43,6 +50,14 @@
   return 1;
 }
 
+sub _challenge_header ($self, %extra) {
+  my @parts;
+  push @parts, qq{resource_metadata="@{[$self->metadata_url]}"} if 
$self->metadata_url;
+  push @parts, qq{error="$extra{error}"}                        if 
$extra{error};
+  push @parts, qq{scope="$extra{scope}"}                        if defined 
$extra{scope};
+  return 'Bearer' . (@parts ? ' ' . join(', ', @parts) : '');
+}
+
 sub _extract_session_id ($self, $c) { return 
$c->req->headers->header('Mcp-Session-Id') }
 
 sub _handle ($self, $data, $context) {
@@ -93,7 +108,7 @@
 
 sub _handle_initialization ($self, $c, $data) {
   my $session_id = random_v4uuid;
-  my $result     = $self->_handle($data, MCP::Server::Context->new);
+  my $result     = $self->_handle($data, MCP::Server::Context->new(scopes => 
$self->_scopes($c)));
   if ($self->streaming) {
     $self->sessions->{$session_id} = MCP::Server::Session->new(id => 
$session_id);
     $self->_start_sweep;
@@ -121,9 +136,21 @@
   }
 
   $c->res->headers->header('Mcp-Session-Id' => $session_id);
-  my $context = MCP::Server::Context->new(transport => $self, session_id => 
$session_id, controller => $c);
+  my $context = MCP::Server::Context->new(
+    transport  => $self,
+    session_id => $session_id,
+    controller => $c,
+    scopes     => $self->_scopes($c)
+  );
   return $c->render(data => '', status => 202) unless defined(my $result = 
$self->_handle($data, $context));
 
+  # Insufficient scope
+  if (my $needed = $context->insufficient_scope) {
+    $c->res->headers->header(
+      'WWW-Authenticate' => $self->_challenge_header(error => 
'insufficient_scope', scope => join(' ', @$needed)));
+    return $c->render(json => $result, status => 403);
+  }
+
   # Sync
   return $c->render(json => $result, status => 200) if !blessed($result) || 
!$result->isa('Mojo::Promise');
 
@@ -133,6 +160,11 @@
   $result->then(sub { $c->write_sse({text => to_json($_[0])})->finish });
 }
 
+sub _scopes ($self, $c) {
+  return undef unless $self->auth;
+  return ($c->stash('mcp.auth') // {})->{scopes} // [];
+}
+
 sub _start_sweep ($self) {
   return if $self->{_sweep_id};
   return unless my $interval = $self->session_timeout;
@@ -150,6 +182,11 @@
   }
 }
 
+sub _unauthorized ($self, $c) {
+  $c->res->headers->header('WWW-Authenticate' => $self->_challenge_header);
+  return $c->render(json => {error => 'Unauthorized'}, status => 401);
+}
+
 1;
 
 =encoding utf8
@@ -179,6 +216,17 @@
 L<MCP::Server::Transport::HTTP> inherits all attributes from 
L<MCP::Server::Transport> and implements the following
 new ones.
 
+=head2 auth
+
+  my $cb = $http->auth;
+  $http  = $http->auth(sub ($c) {...});
+
+Optional callback to authenticate each request before it is dispatched. It 
receives the L<Mojolicious::Controller>
+and returns a hash reference of authentication info on success, or a false 
value to reject the request with a
+C<401> C<WWW-Authenticate> challenge. The C<scopes> key of the returned hash 
reference is made available to handlers
+as L<MCP::Server::Context/"scopes">. Token validation is left to the 
application, so this is where you verify an
+OAuth 2.0 access token; when not set, requests are not authenticated.
+
 =head2 heartbeat
 
   my $seconds = $http->heartbeat;
@@ -188,6 +236,15 @@
 set to C<0> to disable. Useful when running behind reverse proxies that close 
idle connections. Only used when
 L</"streaming"> is enabled.
 
+=head2 metadata_url
+
+  my $url = $http->metadata_url;
+  $http   = 
$http->metadata_url('https://example.com/.well-known/oauth-protected-resource');
+
+URL of the OAuth 2.0 Protected Resource Metadata document. When set, it is 
included as the C<resource_metadata>
+parameter of the C<WWW-Authenticate> challenge sent with C<401> and C<403> 
responses, so clients can discover the
+authorization server. Use an absolute URL so remote clients can fetch it. See 
L<MCP::Server/"oauth_metadata">.
+
 =head2 session_timeout
 
   my $seconds = $http->session_timeout;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Server/Transport/Stdio.pm 
new/MCP-0.12/lib/MCP/Server/Transport/Stdio.pm
--- old/MCP-0.10/lib/MCP/Server/Transport/Stdio.pm      2026-05-06 
22:55:47.000000000 +0200
+++ new/MCP-0.12/lib/MCP/Server/Transport/Stdio.pm      2026-06-19 
20:01:34.000000000 +0200
@@ -9,9 +9,13 @@
 sub handle_requests ($self) {
   my $server = $self->server;
 
+  binmode STDIN,  ':raw';
+  binmode STDOUT, ':raw';
   STDOUT->autoflush(1);
-  while (my $input = <>) {
-    chomp $input;
+
+  my $buffer = '';
+  while (defined(my $input = _read_line(\$buffer))) {
+    next if $input eq '';
     my $request = eval { decode_json($input) };
     next unless my $response = $server->handle($request, 
MCP::Server::Context->new(transport => $self));
 
@@ -22,6 +26,19 @@
   }
 }
 
+sub _read_line ($buffer) {
+  while (index($$buffer, "\n") < 0) {
+    last unless sysread STDIN, my $chunk, 131072;
+    $$buffer .= $chunk;
+  }
+  return undef if $$buffer eq '';
+
+  my $pos  = index($$buffer, "\n");
+  my $line = $pos < 0 ? substr($$buffer, 0, length($$buffer), '') : 
substr($$buffer, 0, $pos + 1, '');
+  $line =~ s/\r?\n?$//;
+  return $line;
+}
+
 sub notify ($self, $session_id, $method, $params = {}) {
   _print_response({jsonrpc => '2.0', method => $method, params => $params});
   return 1;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP/Server.pm 
new/MCP-0.12/lib/MCP/Server.pm
--- old/MCP-0.10/lib/MCP/Server.pm      2026-05-06 22:55:47.000000000 +0200
+++ new/MCP-0.12/lib/MCP/Server.pm      2026-06-19 16:05:31.000000000 +0200
@@ -1,9 +1,10 @@
 package MCP::Server;
 use Mojo::Base 'Mojo::EventEmitter', -signatures;
 
-use List::Util     qw(first);
-use Mojo::JSON     qw(false true);
-use MCP::Constants qw(INVALID_PARAMS INVALID_REQUEST METHOD_NOT_FOUND 
PARSE_ERROR PROTOCOL_VERSION RESOURCE_NOT_FOUND);
+use List::Util qw(first);
+use Mojo::JSON qw(false true);
+use MCP::Constants
+  qw(INSUFFICIENT_SCOPE INVALID_PARAMS INVALID_REQUEST METHOD_NOT_FOUND 
PARSE_ERROR PROTOCOL_VERSION RESOURCE_NOT_FOUND);
 use MCP::Prompt;
 use MCP::Resource;
 use MCP::Server::Transport::HTTP;
@@ -70,6 +71,16 @@
   return $transport->notify_all("notifications/$kind/list_changed");
 }
 
+sub oauth_metadata ($self, %args) {
+  my %scopes;
+  for my $primitive (@{$self->tools}, @{$self->prompts}, @{$self->resources}) {
+    $scopes{$_} = 1 for @{$primitive->scopes};
+  }
+  my $metadata = {%args};
+  $metadata->{scopes_supported} //= [sort keys %scopes] if keys %scopes;
+  return $metadata;
+}
+
 sub prompt ($self, %args) {
   my $prompt = MCP::Prompt->new(%args);
   push @{$self->prompts}, $prompt;
@@ -111,6 +122,7 @@
 sub _handle_prompts_list ($self, $context) {
   my @prompts;
   for my $prompt (@{$self->_prompts($context)}) {
+    next unless $context->has_scope(@{$prompt->scopes});
     my $info = {name => $prompt->name, description => $prompt->description, 
arguments => $prompt->arguments};
     push @prompts, $info;
   }
@@ -121,9 +133,10 @@
 sub _handle_prompts_get ($self, $params, $id, $context) {
   my $name = $params->{name}      // '';
   my $args = $params->{arguments} // {};
-  return _jsonrpc_error(METHOD_NOT_FOUND, "Prompt '$name' not found")
+  return _jsonrpc_error(METHOD_NOT_FOUND, "Prompt '$name' not found", $id)
     unless my $prompt = first { $_->name eq $name } 
@{$self->_prompts($context)};
-  return _jsonrpc_error(INVALID_PARAMS, 'Invalid arguments') if 
$prompt->validate_input($args);
+  if (my $err = $self->_check_scope($prompt, $context, $id)) { return $err }
+  return _jsonrpc_error(INVALID_PARAMS, 'Invalid arguments', $id) if 
$prompt->validate_input($args);
 
   my $result = $prompt->call($args, $context);
   return $result->then(sub { _jsonrpc_response($_[0], $id) }) if 
blessed($result) && $result->isa('Mojo::Promise');
@@ -133,6 +146,7 @@
 sub _handle_resources_list ($self, $context) {
   my @resources;
   for my $resource (@{$self->_resources($context)}) {
+    next unless $context->has_scope(@{$resource->scopes});
     my $info = {
       uri         => $resource->uri,
       name        => $resource->name,
@@ -147,8 +161,9 @@
 
 sub _handle_resources_read ($self, $params, $id, $context) {
   my $uri = $params->{uri} // '';
-  return _jsonrpc_error(RESOURCE_NOT_FOUND, 'Resource not found')
+  return _jsonrpc_error(RESOURCE_NOT_FOUND, 'Resource not found', $id)
     unless my $resource = first { $_->uri eq $uri } 
@{$self->_resources($context)};
+  if (my $err = $self->_check_scope($resource, $context, $id)) { return $err }
 
   my $result = $resource->call($context);
   return $result->then(sub { _jsonrpc_response($_[0], $id) }) if 
blessed($result) && $result->isa('Mojo::Promise');
@@ -158,9 +173,10 @@
 sub _handle_tools_call ($self, $params, $id, $context) {
   my $name = $params->{name}      // '';
   my $args = $params->{arguments} // {};
-  return _jsonrpc_error(METHOD_NOT_FOUND, "Tool '$name' not found")
+  return _jsonrpc_error(METHOD_NOT_FOUND, "Tool '$name' not found", $id)
     unless my $tool = first { $_->name eq $name } @{$self->_tools($context)};
-  return _jsonrpc_error(INVALID_PARAMS, 'Invalid arguments') if 
$tool->validate_input($args);
+  if (my $err = $self->_check_scope($tool, $context, $id)) { return $err }
+  return _jsonrpc_error(INVALID_PARAMS, 'Invalid arguments', $id) if 
$tool->validate_input($args);
 
   my $result = $tool->call($args, $context);
   return $result->then(sub { _jsonrpc_response($_[0], $id) }) if 
blessed($result) && $result->isa('Mojo::Promise');
@@ -170,6 +186,7 @@
 sub _handle_tools_list ($self, $context) {
   my @tools;
   for my $tool (@{$self->_tools($context)}) {
+    next unless $context->has_scope(@{$tool->scopes});
     my $info = {name => $tool->name, description => $tool->description, 
inputSchema => $tool->input_schema};
     if (my $output_schema = $tool->output_schema) { $info->{outputSchema} = 
$output_schema }
 
@@ -181,6 +198,13 @@
   return {tools => \@tools};
 }
 
+sub _check_scope ($self, $primitive, $context, $id) {
+  my $scopes = $primitive->scopes;
+  return undef if $context->has_scope(@$scopes);
+  $context->insufficient_scope($scopes);
+  return _jsonrpc_error(INSUFFICIENT_SCOPE, 'Insufficient scope', $id);
+}
+
 sub _jsonrpc_error ($code, $message, $id = undef) {
   return {jsonrpc => '2.0', id => $id, error => {code => $code, message => 
$message}};
 }
@@ -339,6 +363,17 @@
 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 oauth_metadata
+
+  my $metadata = $server->oauth_metadata(
+    resource              => 'https://example.com/mcp',
+    authorization_servers => ['https://auth.example.com']
+  );
+
+Build an OAuth 2.0 Protected Resource Metadata document from the given fields, 
to be served from
+C</.well-known/oauth-protected-resource>. Unless C<scopes_supported> is 
provided, it is filled in with the sorted
+union of all scopes declared by registered tools, prompts, and resources.
+
 =head2 prompt
 
   my $prompt = $server->prompt(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/lib/MCP.pm new/MCP-0.12/lib/MCP.pm
--- old/MCP-0.10/lib/MCP.pm     2026-05-06 21:53:53.000000000 +0200
+++ new/MCP-0.12/lib/MCP.pm     2026-06-19 16:08:09.000000000 +0200
@@ -1,7 +1,7 @@
 package MCP;
 use Mojo::Base -base, -signatures;
 
-our $VERSION = '0.10';
+our $VERSION = '0.12';
 
 1;
 
@@ -59,8 +59,9 @@
 
   app->start;
 
-Authentication can be added by the web application, just like for any other 
route. To allow for MCP applications to
-scale with prefork web servers, server to client streaming is currentlly 
avoided when possible.
+Authentication can be added by the web application, just like for any other 
route. OAuth scopes can be enforced per
+tool, prompt and resource. To allow for MCP applications to scale with prefork 
web servers, server to client
+streaming is currentlly avoided when possible.
 
 =head3 Stdio Transport
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/t/apps/stdio.pl new/MCP-0.12/t/apps/stdio.pl
--- old/MCP-0.10/t/apps/stdio.pl        2026-05-06 22:55:47.000000000 +0200
+++ new/MCP-0.12/t/apps/stdio.pl        2026-06-19 16:05:31.000000000 +0200
@@ -49,5 +49,14 @@
     return "Echo: $args->{msg}";
   }
 );
+$server->tool(
+  name         => 'echo_scoped',
+  description  => 'Echo the input text, requires a scope',
+  scopes       => ['mcp:read'],
+  input_schema => {type => 'object', properties => {msg => {type => 
'string'}}, required => ['msg']},
+  code         => sub ($tool, $args) {
+    return "Echo: $args->{msg}";
+  }
+);
 
 $server->to_stdio;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/t/auth_scopes_app.t 
new/MCP-0.12/t/auth_scopes_app.t
--- old/MCP-0.10/t/auth_scopes_app.t    1970-01-01 01:00:00.000000000 +0100
+++ new/MCP-0.12/t/auth_scopes_app.t    2026-06-19 16:05:31.000000000 +0200
@@ -0,0 +1,219 @@
+use Mojo::Base -strict, -signatures;
+
+use Test::More;
+
+use Mojolicious::Lite;
+use Test::Mojo;
+use MCP::Client;
+use MCP::Constants qw(PROTOCOL_VERSION);
+use MCP::Server;
+
+my $server = MCP::Server->new;
+
+$server->tool(
+  name   => 'read_tool',
+  scopes => ['mcp:read'],
+  code   => sub ($tool, $args) {
+    return 'Read result';
+  }
+);
+$server->tool(
+  name   => 'write_tool',
+  scopes => ['mcp:write'],
+  code   => sub ($tool, $args) {
+    return 'Write result';
+  }
+);
+$server->tool(
+  name   => 'custom_tool',
+  scopes => ['mcp:something.custom'],
+  code   => sub ($tool, $args) {
+    return 'custom result';
+  }
+);
+$server->on(
+  tools => sub ($server, $tools, $context) {
+    my $c    = $context->controller;
+    my $role = $c ? $c->stash('role') : '';
+    return if $role eq 'admin';
+    @$tools = grep { $_->name ne 'custom_tool' } @$tools;
+  }
+);
+
+$server->prompt(
+  name   => 'read_prompt',
+  scopes => ['mcp:read'],
+  code   => sub ($prompt, $args) {
+    return 'Read prompt';
+  }
+);
+$server->prompt(
+  name   => 'write_prompt',
+  scopes => ['mcp:write'],
+  code   => sub ($prompt, $args) {
+    return 'Write prompt';
+  }
+);
+
+$server->resource(
+  uri    => 'file:///read',
+  scopes => ['mcp:read'],
+  code   => sub ($resource) {
+    return 'Read resource';
+  }
+);
+$server->resource(
+  uri    => 'file:///write',
+  scopes => ['mcp:write'],
+  code   => sub ($resource) {
+    return 'Write resource';
+  }
+);
+
+get '/.well-known/oauth-protected-resource' => sub ($c) {
+  $c->render(
+    json => $server->oauth_metadata(
+      resource              => 'http://example.com/mcp',
+      authorization_servers => ['https://auth.example.com']
+    )
+  );
+};
+
+# Fake token validation, replace with real OAuth access token verification in 
production
+my $tokens = {
+  ro    => {scopes => ['mcp:read'], role => 'user'},
+  rw    => {scopes => ['mcp:read', 'mcp:write', 'mcp:something.custom'], role 
=> 'user'},
+  admin => {scopes => ['mcp:read', 'mcp:write', 'mcp:something.custom'], role 
=> 'admin'}
+};
+my $metadata_url = 'http://example.com/.well-known/oauth-protected-resource';
+any '/mcp' => $server->to_action({
+  auth => sub ($c) {
+    return undef unless ($c->req->headers->authorization // '') =~ 
/^Bearer\s+(\S+)$/;
+    return undef unless my $token = $tokens->{$1};
+    $c->stash(role => $token->{role});
+    return {scopes => $token->{scopes}};
+  },
+  metadata_url => $metadata_url
+});
+
+my $t = Test::Mojo->new;
+
+subtest 'Discovery' => sub {
+  $t->post_ok('/mcp' => json => {})
+    ->status_is(401)
+    ->header_like('WWW-Authenticate' => qr/^Bearer/)
+    ->header_like('WWW-Authenticate' => 
qr/resource_metadata="\Q$metadata_url\E"/);
+  $t->post_ok('/mcp' => {Authorization => 'Bearer bogus'} => json => 
{})->status_is(401);
+
+  $t->get_ok('/.well-known/oauth-protected-resource')
+    ->status_is(200)
+    ->json_is('/scopes_supported'      => ['mcp:read', 'mcp:something.custom', 
'mcp:write'])
+    ->json_is('/authorization_servers' => ['https://auth.example.com']);
+};
+
+subtest 'Read-only token' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'),
+    headers => {Authorization => 'Bearer ro'});
+  $client->initialize_session;
+
+  subtest 'Tools' => sub {
+    my $result = $client->list_tools;
+    is $result->{tools}[0]{name}, 'read_tool', 'read tool present';
+    is $result->{tools}[1],       undef,       'no more tools';
+
+    is $client->call_tool('read_tool')->{content}[0]{text}, 'Read result', 
'read tool call result';
+    eval { $client->call_tool('write_tool') };
+    like $@, qr/403 response/, 'write tool denied';
+  };
+
+  subtest 'Prompts' => sub {
+    my $result = $client->list_prompts;
+    is $result->{prompts}[0]{name}, 'read_prompt', 'read prompt present';
+    is $result->{prompts}[1],       undef,         'no more prompts';
+
+    is $client->get_prompt('read_prompt')->{messages}[0]{content}{text}, 'Read 
prompt', 'read prompt result';
+    eval { $client->get_prompt('write_prompt') };
+    like $@, qr/403 response/, 'write prompt denied';
+  };
+
+  subtest 'Resources' => sub {
+    my $result = $client->list_resources;
+    is $result->{resources}[0]{uri}, 'file:///read', 'read resource present';
+    is $result->{resources}[1],      undef,          'no more resources';
+
+    is $client->read_resource('file:///read')->{contents}[0]{text}, 'Read 
resource', 'read resource result';
+    eval { $client->read_resource('file:///write') };
+    like $@, qr/403 response/, 'write resource denied';
+  };
+};
+
+subtest 'Read-write token' => sub {
+  my $client = MCP::Client->new(ua => $t->ua, url => 
$t->ua->server->url->path('/mcp'),
+    headers => {Authorization => 'Bearer rw'});
+  $client->initialize_session;
+
+  subtest 'Tools' => sub {
+    my $result = $client->list_tools;
+    is $result->{tools}[0]{name}, 'read_tool',  'read tool present';
+    is $result->{tools}[1]{name}, 'write_tool', 'write tool present';
+    is $result->{tools}[2],       undef,        'custom tool hidden by role';
+
+    is $client->call_tool('write_tool')->{content}[0]{text}, 'Write result', 
'write tool call result';
+    eval { $client->call_tool('custom_tool') };
+    like $@, qr/Error -32601/, 'custom tool hidden by role';
+  };
+
+  subtest 'Prompts' => sub {
+    my $result = $client->list_prompts;
+    is $result->{prompts}[0]{name}, 'read_prompt',  'read prompt present';
+    is $result->{prompts}[1]{name}, 'write_prompt', 'write prompt present';
+    is $result->{prompts}[2],       undef,          'no more prompts';
+
+    is $client->get_prompt('write_prompt')->{messages}[0]{content}{text}, 
'Write prompt', 'write prompt result';
+  };
+
+  subtest 'Resources' => sub {
+    my $result = $client->list_resources;
+    is $result->{resources}[0]{uri}, 'file:///read',  'read resource present';
+    is $result->{resources}[1]{uri}, 'file:///write', 'write resource present';
+    is $result->{resources}[2],      undef,           'no more resources';
+
+    is $client->read_resource('file:///write')->{contents}[0]{text}, 'Write 
resource', 'write resource result';
+  };
+};
+
+subtest 'Admin token' => sub {
+  my $client = MCP::Client->new(
+    ua      => $t->ua,
+    url     => $t->ua->server->url->path('/mcp'),
+    headers => {Authorization => 'Bearer admin'}
+  );
+  $client->initialize_session;
+
+  my $result = $client->list_tools;
+  is $result->{tools}[0]{name}, 'read_tool',   'read tool present';
+  is $result->{tools}[1]{name}, 'write_tool',  'write tool present';
+  is $result->{tools}[2]{name}, 'custom_tool', 'custom tool present';
+  is $result->{tools}[3],       undef,         'no more tools';
+
+  is $client->call_tool('custom_tool')->{content}[0]{text}, 'custom result', 
'custom tool call result';
+};
+
+subtest 'Insufficient scope challenge' => sub {
+  my $init = {
+    jsonrpc => '2.0',
+    id      => 1,
+    method  => 'initialize',
+    params  => {protocolVersion => PROTOCOL_VERSION, capabilities => {}, 
clientInfo => {name => 't', version => '1'}}
+  };
+  $t->post_ok('/mcp' => {Authorization => 'Bearer ro'} => json => 
$init)->status_is(200);
+  my $session_id = $t->tx->res->headers->header('Mcp-Session-Id');
+
+  my $call = {jsonrpc => '2.0', id => 2, method => 'tools/call', params => 
{name => 'write_tool', arguments => {}}};
+  $t->post_ok('/mcp' => {Authorization => 'Bearer ro', 'Mcp-Session-Id' => 
$session_id} => json => $call)
+    ->status_is(403)
+    ->header_like('WWW-Authenticate' => qr/error="insufficient_scope"/)
+    ->header_like('WWW-Authenticate' => qr/scope="mcp:write"/);
+};
+
+done_testing;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/t/lib/MCPStdioTest.pm 
new/MCP-0.12/t/lib/MCPStdioTest.pm
--- old/MCP-0.10/t/lib/MCPStdioTest.pm  2026-05-06 21:53:53.000000000 +0200
+++ new/MCP-0.12/t/lib/MCPStdioTest.pm  2026-06-19 20:01:34.000000000 +0200
@@ -32,6 +32,12 @@
   return 1;
 }
 
+sub send_request_crlf ($self, $method, $params) {
+  $self->{timeout}->start(60);
+  $self->{stdin} .= encode_json($self->client->build_request($method, 
$params)) . "\r\n";
+  return 1;
+}
+
 sub run ($self, @command) {
   $self->{run} = start(\@command, \$self->{stdin}, \$self->{stdout}, 
\$self->{stderr}, $self->{timeout} = timeout(60));
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/MCP-0.10/t/stdio.t new/MCP-0.12/t/stdio.t
--- old/MCP-0.10/t/stdio.t      2026-05-06 22:55:47.000000000 +0200
+++ new/MCP-0.12/t/stdio.t      2026-06-19 20:01:34.000000000 +0200
@@ -110,6 +110,22 @@
   is_deeply $res->{result}, {content => [{text => 'Echo: hi', type => 
'text'}], isError => false}, 'tool call result';
 };
 
+subtest 'Scoped tool (no scope enforcement over stdio)' => sub {
+  my $res = $test->request('tools/call', {name => 'echo_scoped', arguments => 
{msg => 'hi'}});
+  is $res->{jsonrpc}, '2.0', 'JSON-RPC version';
+  is $res->{id},      9,     'request id';
+  is_deeply $res->{result}, {content => [{text => 'Echo: hi', type => 
'text'}], isError => false}, 'tool call result';
+};
+
+subtest 'Tool call (CRLF line endings)' => sub {
+  $test->send_request_crlf('tools/call', {name => 'echo', arguments => {msg => 
'hello mojo'}});
+  my $res = $test->read_line;
+  is $res->{jsonrpc}, '2.0', 'JSON-RPC version';
+  is $res->{id},      10,    'request id';
+  is_deeply $res->{result}, {content => [{text => 'Echo: hello mojo', type => 
'text'}], isError => false},
+    'tool call result';
+};
+
 ok $test->stop, 'process stopped';
 
 done_testing;

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.FeKFaY/_old  2026-06-29 17:31:16.307898232 +0200
+++ /var/tmp/diff_new_pack.FeKFaY/_new  2026-06-29 17:31:16.315898505 +0200
@@ -1,6 +1,6 @@
-mtime: 1778141034
-commit: 2743c0c65b98fe23eefe3241e65f3926ea2b56fd6924cd9044f09a4d0c0d5a98
+mtime: 1782729687
+commit: 9b4485a5897cfd345e224a9ec10f306d5a9e94b89cac1e2e69042ec2635014fe
 url: https://src.opensuse.org/perl/perl-MCP
-revision: 2743c0c65b98fe23eefe3241e65f3926ea2b56fd6924cd9044f09a4d0c0d5a98
+revision: 9b4485a5897cfd345e224a9ec10f306d5a9e94b89cac1e2e69042ec2635014fe
 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-06-29 12:41:27.000000000 +0200
@@ -0,0 +1 @@
+.osc

Reply via email to