Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-fastmcp-slim for 
openSUSE:Factory checked in at 2026-07-28 18:17:11
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-fastmcp-slim (Old)
 and      /work/SRC/openSUSE:Factory/.python-fastmcp-slim.new.2004 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-fastmcp-slim"

Tue Jul 28 18:17:11 2026 rev:5 rq:1368132 version:3.4.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-fastmcp-slim/python-fastmcp-slim.changes  
2026-07-09 22:21:11.853839732 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-fastmcp-slim.new.2004/python-fastmcp-slim.changes
        2026-07-28 18:18:49.716461032 +0200
@@ -1,0 +2,12 @@
+Tue Jul 28 06:05:03 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 3.4.5:
+  * Skip unsupported JWKS keys: a single Ed25519 key in a key set
+    made JWTVerifier reject every token, including ones correctly
+    signed by supported keys in the same set
+  * Fix the Azure scope fallback
+  * Serialize deep object query parameters
+  * Make the transformed-tool required order deterministic
+  * Do not mutate the caller's schema in compress_schema
+
+-------------------------------------------------------------------

Old:
----
  fastmcp_slim-3.4.4.tar.gz

New:
----
  fastmcp_slim-3.4.5.tar.gz

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

Other differences:
------------------
++++++ python-fastmcp-slim.spec ++++++
--- /var/tmp/diff_new_pack.EbEyyX/_old  2026-07-28 18:18:51.008506275 +0200
+++ /var/tmp/diff_new_pack.EbEyyX/_new  2026-07-28 18:18:51.008506275 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-fastmcp-slim
-Version:        3.4.4
+Version:        3.4.5
 Release:        0
 Summary:        The fast, Pythonic way to build MCP servers and clients (slim)
 License:        Apache-2.0

++++++ fastmcp_slim-3.4.4.tar.gz -> fastmcp_slim-3.4.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fastmcp_slim-3.4.4/PKG-INFO 
new/fastmcp_slim-3.4.5/PKG-INFO
--- old/fastmcp_slim-3.4.4/PKG-INFO     2020-02-02 01:00:00.000000000 +0100
+++ new/fastmcp_slim-3.4.5/PKG-INFO     2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: fastmcp-slim
-Version: 3.4.4
+Version: 3.4.5
 Summary: The dependency-slim FastMCP package.
 Project-URL: Homepage, https://gofastmcp.com
 Project-URL: Repository, https://github.com/PrefectHQ/fastmcp
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fastmcp_slim-3.4.4/fastmcp/server/auth/providers/azure.py 
new/fastmcp_slim-3.4.5/fastmcp/server/auth/providers/azure.py
--- old/fastmcp_slim-3.4.4/fastmcp/server/auth/providers/azure.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/fastmcp_slim-3.4.5/fastmcp/server/auth/providers/azure.py       
2020-02-02 01:00:00.000000000 +0100
@@ -501,8 +501,12 @@
         Returns:
             List of scopes for Azure token endpoint
         """
-        # Prefix scopes for this API
-        prefixed_scopes = self._prefix_scopes_for_azure(scopes or [])
+        # Prefix scopes for this API. Some clients omit the scope parameter on
+        # the MCP authorization request; use the provider's configured scopes
+        # just like the authorize URL path does.
+        prefixed_scopes = self._prefix_scopes_for_azure(
+            scopes or self.required_scopes or []
+        )
 
         # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000
         if self.additional_authorize_scopes:
@@ -528,9 +532,13 @@
         """
         logger.debug("Base scopes from storage: %s", scopes)
 
+        # Some clients omit the scope parameter on the MCP authorization 
request;
+        # use the provider's configured scopes just like the authorize URL 
path does.
+        requested_scopes = scopes or self.required_scopes or []
+
         # Filter out any additional_authorize_scopes that may have been stored
         additional_scopes_set = set(self.additional_authorize_scopes or [])
-        base_scopes = [s for s in scopes if s not in additional_scopes_set]
+        base_scopes = [s for s in requested_scopes if s not in 
additional_scopes_set]
 
         # Prefix base scopes with identifier_uri for Azure
         prefixed_scopes = self._prefix_scopes_for_azure(base_scopes)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fastmcp_slim-3.4.4/fastmcp/server/auth/providers/jwt.py 
new/fastmcp_slim-3.4.5/fastmcp/server/auth/providers/jwt.py
--- old/fastmcp_slim-3.4.4/fastmcp/server/auth/providers/jwt.py 2020-02-02 
01:00:00.000000000 +0100
+++ new/fastmcp_slim-3.4.5/fastmcp/server/auth/providers/jwt.py 2020-02-02 
01:00:00.000000000 +0100
@@ -347,11 +347,26 @@
         try:
             jwks_data = await self._fetch_jwks()
 
-            # Cache all keys
+            # Cache all usable keys. A key that cannot be converted (e.g. an
+            # unsupported kty like OKP/Ed25519) is skipped rather than failing
+            # the whole set — per RFC 7517 §5, clients should ignore JWKs they
+            # don't understand. Otherwise one exotic key published by the
+            # authorization server would reject every token, including ones
+            # signed by supported keys in the same set (#4515).
             self._jwks_cache = {}
+            skipped_kids: set[str] = set()
             for key_data in jwks_data.get("keys", []):
+                if not isinstance(key_data, dict):
+                    self.logger.debug("Skipping non-object JWKS entry: %r", 
key_data)
+                    continue
                 key_kid = key_data.get("kid")
-                public_key = _jwk_to_pem(key_data)
+                try:
+                    public_key = _jwk_to_pem(key_data)
+                except (JoseError, TypeError, KeyError, ValueError) as e:
+                    self.logger.debug("Skipping unusable JWKS key %r: %s", 
key_kid, e)
+                    if key_kid:
+                        skipped_kids.add(key_kid)
+                    continue
 
                 if key_kid:
                     self._jwks_cache[key_kid] = public_key
@@ -364,6 +379,16 @@
             # Select the appropriate key
             if kid:
                 if kid not in self._jwks_cache:
+                    if kid in skipped_kids:
+                        self.logger.debug(
+                            "JWKS key lookup failed: key ID '%s' is present "
+                            "but its key type is unsupported",
+                            kid,
+                        )
+                        raise ValueError(
+                            f"Key ID '{kid}' found in JWKS but its key type "
+                            "is unsupported"
+                        )
                     self.logger.debug(
                         "JWKS key lookup failed: key ID '%s' not found", kid
                     )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fastmcp_slim-3.4.4/fastmcp/tools/tool_transform.py 
new/fastmcp_slim-3.4.5/fastmcp/tools/tool_transform.py
--- old/fastmcp_slim-3.4.4/fastmcp/tools/tool_transform.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/fastmcp_slim-3.4.5/fastmcp/tools/tool_transform.py      2020-02-02 
01:00:00.000000000 +0100
@@ -718,7 +718,8 @@
         schema = {
             "type": "object",
             "properties": new_props,
-            "required": list(new_required),
+            # Iterate props (not the set) for deterministic ordering
+            "required": [p for p in new_props if p in new_required],
             "additionalProperties": False,
         }
 
@@ -910,7 +911,11 @@
         result = {
             "type": "object",
             "properties": merged_props,
-            "required": list(final_required),
+            # Iterate props (not the set) for deterministic ordering; keep any
+            # required names not present in properties (sorted) rather than
+            # silently dropping them.
+            "required": [p for p in merged_props if p in final_required]
+            + sorted(final_required - set(merged_props)),
             "additionalProperties": False,
         }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/fastmcp_slim-3.4.4/fastmcp/utilities/json_schema.py 
new/fastmcp_slim-3.4.5/fastmcp/utilities/json_schema.py
--- old/fastmcp_slim-3.4.4/fastmcp/utilities/json_schema.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/fastmcp_slim-3.4.5/fastmcp/utilities/json_schema.py     2020-02-02 
01:00:00.000000000 +0100
@@ -1,12 +1,45 @@
 from __future__ import annotations
 
-import copy
 from collections import defaultdict
 from typing import Any
 
 from jsonref import JsonRefError, replace_refs
 
 
+def _copy_schema(schema: dict[str, Any]) -> dict[str, Any]:
+    """Return a deep copy of a JSON schema without recursing.
+
+    `copy.deepcopy` consumes stack frames in proportion to nesting depth, so a
+    deeply nested schema raises `RecursionError` before the traversals in this
+    module can apply their own depth guards — turning a schema that used to
+    compress into one that fails outright. Schemas are plain JSON, so an
+    explicit stack copies the containers at any depth and shares the immutable
+    scalars at the leaves.
+    """
+    root: dict[str, Any] = {}
+    stack: list[tuple[Any, Any]] = [(schema, root)]
+
+    while stack:
+        source, target = stack.pop()
+        if isinstance(source, dict):
+            pairs: list[tuple[Any, Any]] = list(source.items())
+        else:
+            pairs = list(enumerate(source))
+
+        for key, value in pairs:
+            if isinstance(value, dict):
+                child: Any = {}
+                stack.append((value, child))
+            elif isinstance(value, list):
+                child = [None] * len(value)
+                stack.append((value, child))
+            else:
+                child = value
+            target[key] = child
+
+    return root
+
+
 def _defs_have_cycles(defs: dict[str, Any]) -> bool:
     """Check whether any definitions in ``$defs`` form a reference cycle.
 
@@ -348,7 +381,7 @@
     """Return a new schema with *param* removed from `properties`, `required`,
     and (if no longer referenced) `$defs`.
     """
-    schema = copy.deepcopy(schema)
+    schema = _copy_schema(schema)
 
     # ── 1. drop from properties/required ──────────────────────────────
     props = schema.get("properties", {})
@@ -498,6 +531,11 @@
     if not (prune_defs or prune_titles or prune_additional_properties):
         return schema  # Nothing to do
 
+    # Work on a copy so the caller's schema is never mutated (see docstring). 
The
+    # pruning phases below pop keys/$defs in place, which would otherwise 
corrupt a
+    # shared dict such as a live Tool.input_schema passed straight to 
compress_schema.
+    schema = _copy_schema(schema)
+
     # Phase 1: Collect references and apply simple cleanups
     # Track which $defs are referenced from the main schema and from other 
$defs
     root_refs: set[str] = set()  # $defs referenced directly from main schema
@@ -506,6 +544,11 @@
     )  # def A references def B
     defs = schema.get("$defs")
 
+    # Set when the traversal below gives up at its depth limit. Once that
+    # happens the reference scan is incomplete, so we can no longer tell which
+    # definitions are genuinely unused.
+    reference_scan_truncated = False
+
     def traverse_and_clean(
         node: object,
         current_def_name: str | None = None,
@@ -523,7 +566,10 @@
         about) but we skip all cleanups so we don't mutate user data that
         happens to look metadata-shaped.
         """
+        nonlocal reference_scan_truncated
+
         if depth > 50:  # Prevent infinite recursion
+            reference_scan_truncated = True
             return
 
         if isinstance(node, dict):
@@ -647,6 +693,13 @@
         for def_name, def_schema in defs.items():
             traverse_and_clean(def_schema, current_def_name=def_name, 
in_schema=True)
 
+        # An incomplete scan has not seen every $ref, so a definition that 
looks
+        # unused may simply be referenced below the cutoff. Keeping an unused
+        # definition is harmless; dropping a referenced one leaves a dangling
+        # $ref and an invalid schema.
+        if reference_scan_truncated:
+            return schema
+
         # Phase 4: Remove unused definitions
         def is_def_used(def_name: str, visiting: set[str] | None = None) -> 
bool:
             """Check if a definition is used, handling circular references."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/fastmcp_slim-3.4.4/fastmcp/utilities/openapi/director.py 
new/fastmcp_slim-3.4.5/fastmcp/utilities/openapi/director.py
--- old/fastmcp_slim-3.4.4/fastmcp/utilities/openapi/director.py        
2020-02-02 01:00:00.000000000 +0100
+++ new/fastmcp_slim-3.4.5/fastmcp/utilities/openapi/director.py        
2020-02-02 01:00:00.000000000 +0100
@@ -313,13 +313,13 @@
                     if not value:
                         continue
                     if explode:
-                        # form,explode=true on objects: each property becomes
-                        # a separate query parameter.
-                        # e.g. {"R": 100, "G": 200} → R=100&G=200
                         for k, v in value.items():
-                            serialized[_query_scalar_to_str(k)] = 
_query_scalar_to_str(
-                                v
-                            )
+                            # deepObject keeps the parent parameter name;
+                            # form style emits each property as a bare key.
+                            property_name = _query_scalar_to_str(k)
+                            if param_info.style == "deepObject":
+                                property_name = f"{key}[{property_name}]"
+                            serialized[property_name] = _query_scalar_to_str(v)
                     else:
                         style = param_info.style or "form"
                         delimiter = self._STYLE_DELIMITERS.get(style, ",")

Reply via email to