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, ",")