Repository navigation
Conversation
An inactive bearer sent as 403 looks like a permission gap, so clients may keep sending a dead token. The server returns 401 with Bearer invalid_token for rejected presented tokens. A missing bearer gets the metadata challenge without that parameter. The response body keeps introspection-established grant_revoked, grant_expired, and grant_invalid codes. A request with an active package token and no active child grant gets 401. Another active child keeps package access. Status follows the failure point: an inactive client bearer gets 401, while an authenticated owner request that reaches malformed grant state stays 403. Because grant_invalid can describe either case, clients must follow HTTP status. The `codeToStatus` map keeps `grant_invalid` at 403 for errors found after authentication. The test `grant-scoped polyfill state rejects malformed persisted grant bindings as invalid grants` continues to pin the owner-path 403. The /mcp tests assert status, challenge, and body for missing and unknown tokens, revoked and expired grants, and a package whose last child was revoked. The multi-child test verifies that one active child keeps package access. REST auth-gate and metadata tests verify the rejected-token and missing-token challenges. Updated REST tests that pinned 403 for inactive tokens now expect 401: - `revoked grants fail with grant_revoked` - `expired grants fail with grant_expired and preserve correlation headers` - `native client reads reject grants missing structured storage bindings` - `polyfill client reads fail connector-first when the persisted storage binding points to an unknown connector` - `polyfill client introspection and reads reject persisted grants whose manifest_version no longer matches the current manifest` - `native client introspection and reads reject persisted grants whose manifest_version no longer matches the current manifest` - `auth-gate client read failures emit correlated query.rejected artifacts on the grant timeline` Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI
Return authentication_error with a generic message when introspection rejects an inactive token for malformed grant state. Keep grant_invalid in the internal rejection event and record the actual auth-gate status there. Authenticated owner requests with malformed grant state remain 403. Update CLI checks for inactive-token HTTP 401 and exit 3. Give expired /mcp coverage its own test so it fails independently. Align the remaining revoked-grant test expectations and troubleshooting guidance with 401 invalid_token while retaining grant_revoked in the error body. Signed-off-by: Tim Nunamaker <tnunamak@gmail.com> Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
Package token calls previously defaulted to every grant in a package, which made the read target implicit. The MCP server now requires a valid `grant_id` on all six data tools and dispatches each call through only that child’s RS client. Missing, malformed, and unknown selectors fail before any RS request and name `list_package_children` as recovery guidance; that discovery tool ships in a later PR. Client-token schemas and routing stay unchanged. This removes the package merge layer, including merged schema and search output, record deduplication, `source_mix`, summed scan counts, and combined `has_more`. It also removes the implicit single-child fallback and all-child default, and suppresses package resource templates. Both token lists stay under the 24,576-byte tools budget. The old package adapter suite’s merge, fan-out, selector, and event-routing tests are replaced by child-client construction tests. The hosted OAuth multi-source picker, revoked-child, and `query_records`-by-`connection_id` tests now cover required selection, revocation isolation, and real sibling-record rejection; refresh checks both original child grants and re-consent checks the replacement child IDs. Package-profile tests cover all six tools, both children, invalid selectors, and downstream `grant_invalid` propagation with one selected-child call and zero sibling calls. Assisted-by: AI Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
This was referenced Oct 15, 2026
tnunamak
force-pushed
the
mcp/e4-invalid-token-401
branch
from
October 15, 2026 03:40
7bb3ac0 to
6c943ca
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A grant package is one access token that covers several child grants. Today a hosted MCP call with a package token picks its child implicitly: with one child it falls back to that child, and otherwise it reads every child and merges the results (deduplicating hits, applying one global limit, summing counts). The MCP profile (§3) forbids both: with a package token the child is always named, and no result is computed across children.
For package tokens, each of the six data tools (
schema,query_records,aggregate,search,fetch,read_record_field) now declares a requiredgrant_idand sends the call to exactly that child's resource-server client. The server checksgrant_idbefore any resource-server call and returns a tool result withisError: truewhen it is missing, not a string, malformed or not a child of the token; the error text tells the model how to recover. A validgrant_idis any non-empty string of[A-Za-z0-9._~-](access extension §5.3). Ordinary client tokens keep their tool list and routing unchanged, and rejectgrant_id. Package resource templates are removed so no read can bypass the selector.The cross-child merge layer is deleted from
server/package-rs-client.ts(about 1,300 lines, plus the 39 unit tests that pinned it);vendor/mcp-server/src/package-profile.tsis new and holds the selection logic, ported from prototype PR #358 with its experiment flag and itsgrt_-only ID pattern removed. The server instructions and tool descriptions now tell a package-token model to choose a child bygrant_id.Consequence for users: a package-token client that relied on a merged cross-child search or schema now has to call once per child. Child discovery (
list_package_children, named in the error text) and thepackage_child_inactiveresult ship in the next PR; until then a client learns its child ids from the authorization result.Tests:
package-profile.test.tsandhosted-mcp-oauth.test.tscover missing, malformed and unknown selectors and a valid read of child A and child B for every tool (30 cases that fail on the base), a sibling record that stays unreadable after an own-child read succeeds, a revoked child, a downstream403 grant_invalidthat touches no sibling, both children surviving a token refresh, and re-consent. 508 tests and the type check pass;tools/listis 22,580 bytes for client tokens and 23,961 for package tokens, under the 24,576-byte budget. An independent review by a different model ran 90 hostile selectors (prototype keys, whitespace, Unicode, traversal, long strings); none reached a resource-server client.Landing order: do not merge this PR without the child-discovery PR (
list_package_childrenandpackage_child_inactive, the next in the stack). Alone, it would leave a package-token client that has to sendgrant_idwith no way to learn a valid value. Merge them together, or discovery first.What an existing client sees after this merge: a session on an ordinary client token sees no change (same tools, schemas and results). A session on a package token sees
grant_idas a required argument on the six data tools intools/list, so a model that reads the schema supplies it; once the discovery PR is in, the tool list also offerslist_package_childrento find the value. A client that sends nogrant_idgets a tool error naming how to recover instead of a merged result.This PR is stacked on #381 (the 401
invalid_tokenchange) because both edit the hosted test file; retarget it after #381 merges. Not verified: the unchangedtool-footprinttest still fails on the base because it reads an operator document missing from this repository.Assisted-by: AI