fix: include RFC 6750 scope parameter in insufficient_scope challenge#3130
fix: include RFC 6750 scope parameter in insufficient_scope challenge#3130vishnujayvel wants to merge 1 commit into
Conversation
RequireAuthMiddleware._send_auth_error built the WWW-Authenticate header for both the 401 invalid_token and 403 insufficient_scope cases without ever including the scope attribute, even though required_scopes was already available on the middleware instance. RFC 6750 3.1 says the insufficient_scope response "MAY include the 'scope' attribute with the scope necessary to access the protected resource." Emitting it lets a client discover the correct scope to request directly from the challenge instead of falling back to protected-resource-metadata scopes_supported, which the SDK's client already treats as a lower-priority signal in get_client_metadata_scopes(). The 401 cases (no credentials, invalid token) are left unchanged: per RFC 6750 3.1 scope is specific to the insufficient_scope error and doesn't apply when the resource server hasn't yet determined what scope would even satisfy the request. Reported-by: velias Github-Issue: modelcontextprotocol#3103
There was a problem hiding this comment.
36 issues found across 129 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="examples/snippets/servers/prompt_embedded_resources.py">
<violation number="1" location="examples/snippets/servers/prompt_embedded_resources.py:11">
P2: Repeated prompt requests can retain open file descriptors until garbage collection, eventually exhausting descriptors on long-lived servers. Read through a context manager so each request closes its file deterministically.</violation>
</file>
<file name=".github/workflows/weekly-lockfile-update.yml">
<violation number="1" location=".github/workflows/weekly-lockfile-update.yml:28">
P2: A failed dependency resolution is treated as success because this pipeline returns `tee`'s exit status. Enable `pipefail` so no PR is created from a failed `uv lock --upgrade` run.</violation>
</file>
<file name="examples/snippets/servers/tool_errors.py">
<violation number="1" location="examples/snippets/servers/tool_errors.py:27">
P1: Clients can use `read_config` to read any file readable by the server process, exposing configuration and credentials when this documented example is run. Use a non-filesystem operation to demonstrate unhandled exceptions, or constrain paths to an explicitly configured safe directory.</violation>
</file>
<file name="examples/snippets/servers/elicitation_enum.py">
<violation number="1" location="examples/snippets/servers/elicitation_enum.py:10">
P2: A nonconforming client can submit any string and receive `You picked: <value>`, because the enum here is presentation/schema metadata rather than Pydantic validation. Add a validation constraint while retaining the enum metadata so accepted results are restricted to the displayed choices.</violation>
</file>
<file name="docs/authorization.md">
<violation number="1" location="docs/authorization.md:43">
P2: Running this server advertises protected-resource metadata for port 3001 while it listens at `http://localhost:8000/mcp`; OAuth clients cannot discover its authorization server. Match `resource_server_url` to the configured/default streamable HTTP endpoint.</violation>
</file>
<file name="examples/snippets/servers/embedded_resource_results.py">
<violation number="1" location="examples/snippets/servers/embedded_resource_results.py:10">
P1: A tool caller can read arbitrary server-local files (for example, environment files or credentials) because `path` is used directly. Restrict resolved paths to an explicitly configured configuration directory before opening them.</violation>
<violation number="2" location="examples/snippets/servers/embedded_resource_results.py:15">
P2: Relative paths and paths containing URI-reserved characters produce invalid or differently addressed `file:` URIs, so clients cannot reliably identify the returned resource. Build the URI from a resolved path with `Path(path).resolve().as_uri()`.</violation>
</file>
<file name="examples/snippets/servers/set_logging_level.py">
<violation number="1" location="examples/snippets/servers/set_logging_level.py:13">
P2: `logging/setLevel` succeeds but does not alter any logging behavior: this assignment only updates unread module state. Apply `level` to the server/application logger (or show the state being used) so the example implements its documented purpose.</violation>
</file>
<file name="docs/experimental/tasks-server.md">
<violation number="1" location="docs/experimental/tasks-server.md:64">
P3: Stateful Streamable HTTP reconnects reuse an active MCP session ID, so those clients retain the same task scope and can still access their tasks. Describe this as creating a new session, not reconnecting, to avoid documenting a false limitation.</violation>
</file>
<file name="examples/snippets/servers/resource_subscriptions.py">
<violation number="1" location="examples/snippets/servers/resource_subscriptions.py:8">
P1: Clients will not discover subscription support: this handler is never reflected in `ServerCapabilities.resources.subscribe`, so compliant clients will not send `resources/subscribe`. Update capability construction to advertise `subscribe=True` when a subscribe handler is registered (and include a resource-list handler in a runnable example).</violation>
<violation number="2" location="examples/snippets/servers/resource_subscriptions.py:11">
P2: Resource subscriptions are not tracked per client: every subscribe/unsubscribe uses the same literal `"current_session"`. With two clients on one URI, either client’s unsubscribe removes the shared entry, so subsequent updates can omit the other still-subscribed client. This example should either model a single subscription per URI without claiming per-session tracking, or obtain and store a real session identity through the surrounding connection/session management.</violation>
</file>
<file name="src/mcp/client/auth/oauth2.py">
<violation number="1" location="src/mcp/client/auth/oauth2.py:276">
P1: Mismatched PRM can still be accepted when its `resource` is a same-origin parent (or differs only by query), then its authorization server metadata is trusted. RFC 9728 requires an identical resource identifier here; compare exact canonical resource values instead of hierarchical authorization matching.</violation>
</file>
<file name="docs/low-level-server.md">
<violation number="1" location="docs/low-level-server.md:45">
P3: The lifespan example is not type-safe: `dict[str, Any]` makes `ctx.lifespan_context["db"]` an `Any`. Use a `TypedDict` or dataclass containing `db: Database` so the documented type-safety benefit is actually demonstrated.</violation>
</file>
<file name="examples/snippets/servers/binary_resources.py">
<violation number="1" location="examples/snippets/servers/binary_resources.py:9">
P2: Reading `images://logo.png` always raises `FileNotFoundError` in the checked-out example because no `logo.png` is included. Add the image asset alongside the example or make the snippet return bundled/generated PNG bytes so the documented resource can be read.</violation>
</file>
<file name="src/mcp/server/experimental/task_result_handler.py">
<violation number="1" location="src/mcp/server/experimental/task_result_handler.py:49">
P2: The new security guidance overstates default isolation: unscoped task IDs remain retrievable by any session, so callers may incorrectly treat explicit/custom IDs as session-private. Qualify this guarantee to session-scoped IDs and retain the custom-handler access-control warning.</violation>
</file>
<file name="scripts/build-docs.sh">
<violation number="1" location="scripts/build-docs.sh:39">
P1: Docs deployment fails when building the non-trigger branch: this fetch leaves its commit only in `FETCH_HEAD`, while the following worktree command resolves `origin/${branch}`. Fetch into the matching remote-tracking ref before adding the worktree.</violation>
<violation number="2" location="scripts/build-docs.sh:47">
P1: The `/v2` build bypasses main's required Zensical recipe. Main generates its API pages and concrete config in `scripts/docs/build.sh`, so invoking `mkdocs` directly leaves the deployed v2 docs incomplete (or fails). Detect and run that branch-local build script, then copy its local `site/` to `$dest`; retain the MkDocs path for v1.</violation>
</file>
<file name="docs/server.md">
<violation number="1" location="docs/server.md:688">
P2: The thumbnail example labels raw pixel bytes as PNG, so clients cannot decode the returned image. Encode the thumbnail into a PNG buffer before constructing `Image`.</violation>
<violation number="2" location="docs/server.md:1017">
P2: Choosing an alternative date is confirmed without running the availability check again, so a fully booked alternative can be reported as successfully booked. Route the accepted alternative back through the availability flow (preserving `time`, `party_size`, and `ctx`) before confirming it.
(Based on your team's feedback about revalidating accepted alternative dates.) [FEEDBACK_USED]</violation>
<violation number="3" location="docs/server.md:1293">
P2: Copying this session-notification example raises `NameError` because `AnyUrl` is never imported in its code block. Add `from pydantic import AnyUrl` with the snippet imports.</violation>
<violation number="4" location="docs/server.md:1553">
P2: Allowing an origin in CORS does not make it reachable when FastMCP's DNS-rebinding middleware still rejects that origin. Document matching `TransportSecuritySettings` for the browser origin and deployment host alongside the CORS setup, so the advertised cross-origin configuration actually works.</violation>
<violation number="5" location="docs/server.md:1555">
P1: Browser Streamable HTTP requests will fail CORS preflight because this config omits `allow_headers` for `Content-Type`, `Mcp-Session-Id`, and `Mcp-Protocol-Version`; exposing `Mcp-Session-Id` only permits reading the response. Document those request headers and matching `TransportSecuritySettings` allowed host/origin settings for deployed browser origins.
(Based on your team's feedback about ASGI browser CORS contract.) [FEEDBACK_USED]</violation>
</file>
<file name="src/mcp/server/streamable_http_manager.py">
<violation number="1" location="src/mcp/server/streamable_http_manager.py:80">
P2: FastMCP deployments cannot configure this new timeout: `FastMCP.streamable_http_app()` never forwards one to `StreamableHTTPSessionManager`. Add a `session_idle_timeout` FastMCP setting/constructor argument and pass it through when creating the manager.</violation>
</file>
<file name="src/mcp/server/lowlevel/experimental.py">
<violation number="1" location="src/mcp/server/lowlevel/experimental.py:85">
P2: Servers with only list/cancel task handlers now advertise task-augmented `tools/call`, so clients can send calls the server has not declared it can handle. Gate `tasks.requests.tools.call` on actual task-augmented tool-call support, rather than any task handler.</violation>
<violation number="2" location="src/mcp/server/lowlevel/experimental.py:221">
P2: A single `tasks/list` request now reads every page in the shared store, including other sessions' tasks, before responding. Large or continuously growing stores can make an empty listing expensive or non-terminating; preserve bounded pagination with a session-safe opaque cursor.</violation>
</file>
<file name="src/mcp/server/auth/middleware/bearer_auth.py">
<violation number="1" location="src/mcp/server/auth/middleware/bearer_auth.py:122">
P2: Configured scope names containing a quote or backslash now produce an invalid `WWW-Authenticate` header and clients can misparse the required scope. Validate required scopes against RFC 6750's scope-value character set before constructing this challenge.</violation>
</file>
<file name="docs/client.md">
<violation number="1" location="docs/client.md:348">
P3: Parsing-results docs can drift from the runnable example because this block bypasses the repository’s snippet sync check. Wrap `examples/snippets/clients/parsing_tool_results.py` in the same `snippet-source` block used by the preceding examples.</violation>
</file>
<file name="docs/hooks/llms_txt.py">
<violation number="1" location="docs/hooks/llms_txt.py:100">
P2: Nested snippet directives are emitted literally into generated LLM artifacts because this check never inspects `resolved`. Check the post-substitution text so every remaining marker fails the build.
(Based on your team's feedback about guarding unconsumed snippet markers.) [FEEDBACK_USED]</violation>
</file>
<file name=".github/actions/conformance/client.py">
<violation number="1" location=".github/actions/conformance/client.py:319">
P3: Auth conformance runs leave the supplied HTTPX connection pool unclosed and bypass shared redirect/timeout defaults, because `streamable_http_client` does not manage a provided `http_client`. Create it with `create_mcp_http_client` and own it with an async context around the transport.
(Based on your team's feedback about shared AsyncClient defaults.) [FEEDBACK_USED]</violation>
<violation number="2" location=".github/actions/conformance/client.py:355">
P2: A nonresponsive server can make this client exceed its 30-second conformance-process contract, since scenario requests use `ClientSession`'s unbounded default read timeout. Apply one overall deadline to every dispatched scenario, including the default auth path.</violation>
</file>
<file name=".github/workflows/deploy-docs.yml">
<violation number="1" location=".github/workflows/deploy-docs.yml:36">
P1: Docs deployment fails on either branch: the combined build needs both remote-tracking refs, but checkout fetches only the triggering branch. Fetch full history/refs before running `build-docs.sh`.</violation>
</file>
<file name="src/mcp/shared/session.py">
<violation number="1" location="src/mcp/shared/session.py:450">
P1: A request started while disconnect cleanup is awaiting an earlier waiter can miss this one-time snapshot. It is then removed by `clear()` without receiving `CONNECTION_CLOSED` or having its sender closed, so a no-timeout `send_request()` can hang after the connection has ended. Consider marking the session closed and rejecting/terminating requests created during cleanup, or draining entries added while notifying pending requests rather than clearing unseen streams.</violation>
</file>
<file name="VERSIONING.md">
<violation number="1" location="VERSIONING.md:38">
P3: The documented deprecation mechanism will usually be invisible to SDK consumers: Python ignores `DeprecationWarning` by default outside `__main__`. Since this policy relies on the warning to communicate an upcoming removal, consider using a warning category shown to application users (such as `FutureWarning`) or explicitly require/document an enabled warning filter.</violation>
</file>
<file name="src/mcp/server/stdio.py">
<violation number="1" location="src/mcp/server/stdio.py:47">
P2: Malformed UTF-8 inside a JSON string is now silently accepted as a different MCP message. For example, an invalid byte in `method` is replaced with `�`, and `JSONRPCRequest.method` accepts that arbitrary string, so the reader emits a `SessionMessage` instead of the parse exception expected for invalid wire input. Consider preserving/detecting decode errors per line and sending them to `read_stream_writer` rather than using replacement decoding.</violation>
</file>
<file name="scripts/update_doc_snippets.py">
<violation number="1" location="scripts/update_doc_snippets.py:95">
P3: The contributor instructions now invoke a script that no longer exists, so developers following the documented snippet-update step will get a file-not-found error. Please update `CONTRIBUTING.md` to use `scripts/update_doc_snippets.py` (and adjust its wording to cover documentation snippets).</violation>
</file>
<file name="src/mcp/server/experimental/task_scope.py">
<violation number="1" location="src/mcp/server/experimental/task_scope.py:33">
P2: Explicit task IDs that happen to match this grammar are treated as generated session-scoped IDs. Their prefix will normally not match the creating session, so the creator cannot get, retrieve, cancel, or list a task it explicitly created. Consider reserving and rejecting this ID form for explicit IDs, or recording generated-task provenance separately instead of inferring it from arbitrary user-supplied strings.</violation>
</file>
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
| """Read a configuration file.""" | ||
| # If this raises FileNotFoundError, the client receives an | ||
| # error response like "Error executing tool read_config: ..." | ||
| with open(path) as f: |
There was a problem hiding this comment.
P1: Clients can use read_config to read any file readable by the server process, exposing configuration and credentials when this documented example is run. Use a non-filesystem operation to demonstrate unhandled exceptions, or constrain paths to an explicitly configured safe directory.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/snippets/servers/tool_errors.py, line 27:
<comment>Clients can use `read_config` to read any file readable by the server process, exposing configuration and credentials when this documented example is run. Use a non-filesystem operation to demonstrate unhandled exceptions, or constrain paths to an explicitly configured safe directory.</comment>
<file context>
@@ -0,0 +1,49 @@
+ """Read a configuration file."""
+ # If this raises FileNotFoundError, the client receives an
+ # error response like "Error executing tool read_config: ..."
+ with open(path) as f:
+ return f.read()
+
</file context>
| @mcp.tool() | ||
| def read_config(path: str) -> EmbeddedResource: | ||
| """Read a config file and return it as an embedded resource.""" | ||
| with open(path) as f: |
There was a problem hiding this comment.
P1: A tool caller can read arbitrary server-local files (for example, environment files or credentials) because path is used directly. Restrict resolved paths to an explicitly configured configuration directory before opening them.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/snippets/servers/embedded_resource_results.py, line 10:
<comment>A tool caller can read arbitrary server-local files (for example, environment files or credentials) because `path` is used directly. Restrict resolved paths to an explicitly configured configuration directory before opening them.</comment>
<file context>
@@ -0,0 +1,19 @@
+@mcp.tool()
+def read_config(path: str) -> EmbeddedResource:
+ """Read a config file and return it as an embedded resource."""
+ with open(path) as f:
+ content = f.read()
+ return EmbeddedResource(
</file context>
| subscriptions: dict[str, set[str]] = {} # uri -> set of session ids | ||
|
|
||
|
|
||
| @server.subscribe_resource() |
There was a problem hiding this comment.
P1: Clients will not discover subscription support: this handler is never reflected in ServerCapabilities.resources.subscribe, so compliant clients will not send resources/subscribe. Update capability construction to advertise subscribe=True when a subscribe handler is registered (and include a resource-list handler in a runnable example).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/snippets/servers/resource_subscriptions.py, line 8:
<comment>Clients will not discover subscription support: this handler is never reflected in `ServerCapabilities.resources.subscribe`, so compliant clients will not send `resources/subscribe`. Update capability construction to advertise `subscribe=True` when a subscribe handler is registered (and include a resource-list handler in a runnable example).</comment>
<file context>
@@ -0,0 +1,18 @@
+subscriptions: dict[str, set[str]] = {} # uri -> set of session ids
+
+
+@server.subscribe_resource()
+async def handle_subscribe(uri) -> None:
+ """Handle a client subscribing to a resource."""
</file context>
| if not prm_resource: | ||
| return # pragma: no cover | ||
| default_resource = resource_url_from_server_url(self.context.server_url) | ||
| if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource): |
There was a problem hiding this comment.
P1: Mismatched PRM can still be accepted when its resource is a same-origin parent (or differs only by query), then its authorization server metadata is trusted. RFC 9728 requires an identical resource identifier here; compare exact canonical resource values instead of hierarchical authorization matching.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/auth/oauth2.py, line 276:
<comment>Mismatched PRM can still be accepted when its `resource` is a same-origin parent (or differs only by query), then its authorization server metadata is trusted. RFC 9728 requires an identical resource identifier here; compare exact canonical resource values instead of hierarchical authorization matching.</comment>
<file context>
@@ -267,6 +267,15 @@ def __init__(
+ if not prm_resource:
+ return # pragma: no cover
+ default_resource = resource_url_from_server_url(self.context.server_url)
+ if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource):
+ raise OAuthFlowError(f"Protected resource {prm_resource} does not match expected {default_resource}")
+
</file context>
| if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource): | |
| if prm_resource != default_resource: |
| local branch="$1" worktree="$2" dest="$3" | ||
|
|
||
| echo "=== Building docs for ${branch} ===" | ||
| git fetch origin "$branch" |
There was a problem hiding this comment.
P1: Docs deployment fails when building the non-trigger branch: this fetch leaves its commit only in FETCH_HEAD, while the following worktree command resolves origin/${branch}. Fetch into the matching remote-tracking ref before adding the worktree.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/build-docs.sh, line 39:
<comment>Docs deployment fails when building the non-trigger branch: this fetch leaves its commit only in `FETCH_HEAD`, while the following worktree command resolves `origin/${branch}`. Fetch into the matching remote-tracking ref before adding the worktree.</comment>
<file context>
@@ -0,0 +1,54 @@
+ local branch="$1" worktree="$2" dest="$3"
+
+ echo "=== Building docs for ${branch} ==="
+ git fetch origin "$branch"
+ git worktree remove --force "$worktree" 2>/dev/null || true
+ rm -rf "$worktree"
</file context>
| git fetch origin "$branch" | |
| git fetch origin "$branch:refs/remotes/origin/$branch" |
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]: |
There was a problem hiding this comment.
P3: The lifespan example is not type-safe: dict[str, Any] makes ctx.lifespan_context["db"] an Any. Use a TypedDict or dataclass containing db: Database so the documented type-safety benefit is actually demonstrated.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/low-level-server.md, line 45:
<comment>The lifespan example is not type-safe: `dict[str, Any]` makes `ctx.lifespan_context["db"]` an `Any`. Use a `TypedDict` or dataclass containing `db: Database` so the documented type-safety benefit is actually demonstrated.</comment>
<file context>
@@ -1,5 +1,490 @@
+
+
+@asynccontextmanager
+async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]:
+ """Manage server startup and shutdown lifecycle."""
+ # Initialize resources on startup
</file context>
| When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs. | ||
|
|
||
| ```python | ||
| """examples/snippets/clients/parsing_tool_results.py""" |
There was a problem hiding this comment.
P3: Parsing-results docs can drift from the runnable example because this block bypasses the repository’s snippet sync check. Wrap examples/snippets/clients/parsing_tool_results.py in the same snippet-source block used by the preceding examples.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/client.md, line 348:
<comment>Parsing-results docs can drift from the runnable example because this block bypasses the repository’s snippet sync check. Wrap `examples/snippets/clients/parsing_tool_results.py` in the same `snippet-source` block used by the preceding examples.</comment>
<file context>
@@ -0,0 +1,410 @@
+When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.
+
+```python
+"""examples/snippets/clients/parsing_tool_results.py"""
+
+import asyncio
</file context>
|
|
||
| async def _run_auth_session(server_url: str, oauth_auth: OAuthClientProvider) -> None: | ||
| """Common session logic for all OAuth flows.""" | ||
| client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0) |
There was a problem hiding this comment.
P3: Auth conformance runs leave the supplied HTTPX connection pool unclosed and bypass shared redirect/timeout defaults, because streamable_http_client does not manage a provided http_client. Create it with create_mcp_http_client and own it with an async context around the transport.
(Based on your team's feedback about shared AsyncClient defaults.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/actions/conformance/client.py, line 319:
<comment>Auth conformance runs leave the supplied HTTPX connection pool unclosed and bypass shared redirect/timeout defaults, because `streamable_http_client` does not manage a provided `http_client`. Create it with `create_mcp_http_client` and own it with an async context around the transport.
(Based on your team's feedback about shared AsyncClient defaults.) </comment>
<file context>
@@ -0,0 +1,367 @@
+
+async def _run_auth_session(server_url: str, oauth_auth: OAuthClientProvider) -> None:
+ """Common session logic for all OAuth flows."""
+ client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)
+ async with streamable_http_client(url=server_url, http_client=client) as (read_stream, write_stream, _):
+ async with ClientSession(
</file context>
| ## How Breaking Changes Are Communicated | ||
|
|
||
| 1. **Changelog**: All breaking changes are documented in the GitHub release notes with migration instructions. | ||
| 2. **Deprecation**: When feasible, APIs are deprecated for at least one minor release before removal using `warnings.warn()` with `DeprecationWarning`, which surfaces warnings at runtime and through static analysis tooling. |
There was a problem hiding this comment.
P3: The documented deprecation mechanism will usually be invisible to SDK consumers: Python ignores DeprecationWarning by default outside __main__. Since this policy relies on the warning to communicate an upcoming removal, consider using a warning category shown to application users (such as FutureWarning) or explicitly require/document an enabled warning filter.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At VERSIONING.md, line 38:
<comment>The documented deprecation mechanism will usually be invisible to SDK consumers: Python ignores `DeprecationWarning` by default outside `__main__`. Since this policy relies on the warning to communicate an upcoming removal, consider using a warning category shown to application users (such as `FutureWarning`) or explicitly require/document an enabled warning filter.</comment>
<file context>
@@ -0,0 +1,40 @@
+## How Breaking Changes Are Communicated
+
+1. **Changelog**: All breaking changes are documented in the GitHub release notes with migration instructions.
+2. **Deprecation**: When feasible, APIs are deprecated for at least one minor release before removal using `warnings.warn()` with `DeprecationWarning`, which surfaces warnings at runtime and through static analysis tooling.
+3. **Migration guide**: Major version releases include a migration guide describing what changed and how to update.
+4. **PR labels**: Pull requests containing breaking changes are labeled with `breaking change`.
</file context>
|
|
||
| def update_readme_snippets(readme_path: Path = Path("README.md"), check_mode: bool = False) -> bool: | ||
| """Update code snippets in README.md with live code from source files. | ||
| def update_doc_snippets(doc_path: Path, check_mode: bool = False) -> bool: |
There was a problem hiding this comment.
P3: The contributor instructions now invoke a script that no longer exists, so developers following the documented snippet-update step will get a file-not-found error. Please update CONTRIBUTING.md to use scripts/update_doc_snippets.py (and adjust its wording to cover documentation snippets).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/update_doc_snippets.py, line 95:
<comment>The contributor instructions now invoke a script that no longer exists, so developers following the documented snippet-update step will get a file-not-found error. Please update `CONTRIBUTING.md` to use `scripts/update_doc_snippets.py` (and adjust its wording to cover documentation snippets).</comment>
<file context>
@@ -92,21 +92,21 @@ def process_snippet_block(match: re.Match[str], check_mode: bool = False) -> str
-def update_readme_snippets(readme_path: Path = Path("README.md"), check_mode: bool = False) -> bool:
- """Update code snippets in README.md with live code from source files.
+def update_doc_snippets(doc_path: Path, check_mode: bool = False) -> bool:
+ """Update code snippets in a documentation file with live code from source files.
</file context>
[v1.x] fix: include RFC 6750
scopeparameter in insufficient_scope challengeFixes #3103
Problem
RequireAuthMiddleware._send_auth_errorbuilds theWWW-Authenticatechallenge forboth the 401 (
invalid_token) and 403 (insufficient_scope) responses, but neverincludes the
scopeattribute — even whenrequired_scopesis already known on themiddleware instance.
Per RFC 6750 §3.1, the
insufficient_scoperesponse:Without
scopein the challenge, a client can't discover the required scope directlyfrom the 403 response. The SDK's own client already implements the consumer side of
this —
extract_scope_from_www_auth()/get_client_metadata_scopes()treat thechallenge's
scopeas the highest-priority source when handling step-upauthorization — but the server never emits it, so that path is always empty and the
client falls back to the lower-priority protected-resource-metadata
scopes_supported.Fix
_send_auth_errornow accepts an optionalscopeargument and appendsscope="..."to the challenge when set. It's only passed from theinsufficient_scope(403) call site, using the fullrequired_scopeslist.The 401 paths (missing/invalid token) are deliberately left unchanged. RFC 6750 §3.1
draws a line between "no authentication info at all" (server SHOULD NOT include error
info) and
invalid_tokenversusinsufficient_scope—scopeis specific to thelatter, since on a bare 401 the client hasn't necessarily attempted the resource with
any token yet, and there's no established "the request lacked X" scope gap to report
back.
Example, for a server configured with
required_scopes=["api.read"]:Testing
test_missing_required_scope_includes_scope_in_challenge, asserting theexact
WWW-Authenticateheader value on a 403 with multiple required scopes.test_no_user_challenge_omits_scope, asserting the 401 "no user" challengedoes not carry a
scopeattribute, to pin the 401/403 distinction.fail with the expected diff (missing
scope="..."), while the negative-case testis unaffected.
uv run --frozen pytest(full suite): 1150 passed, 95 skipped, 1 xfailed.uv run --frozen ruff check ./ruff format --check .: clean.uv run --frozen pyright: 0 errors.uv run --frozen coverage run -m pytest ... && coverage report: 100% (repo requiresfail_under = 100).This targets
v1.x(non-breaking bug fix) rather thanmain, per this repo'sbranching table in
AGENTS.md. The same gap exists onmain/v2 in the equivalentcode path; happy to open a follow-up there if a maintainer wants it, but wanted to
keep this PR scoped to the fix under discussion in the issue.
Credit to @velias for the clear diagnosis and root-cause writeup in the issue.
AI assistance disclosure: drafted with AI assistance (Claude), reviewed and owned
by me before submission, per this repo's AI-Assisted Contributions policy.