feat(nitro): gate definitions on oauth scopes - #334
Open
HugoRCD wants to merge 1 commit into
Open
Conversation
All three helpers take `scopes`. `requireScopes` wraps the handler each `build()` pushes, reads the verified claims off `event.context.oauth`, and throws a JSON-RPC `-32003` naming the scopes that were missing. Scopes come from `scope` (space-delimited, RFC 6749) and `scp` (a string or an array, as Okta and Entra ID send it). It fails closed: a definition that declares scopes on an endpoint with no OAuth has nothing to satisfy them, so every call is refused. The gate is on the call, not the listing, and that is the engine's order rather than a preference — a handler's options resolve before the request is authenticated, so nothing that builds a listing has seen the token yet. A scoped definition therefore stays visible in `tools/list`, with its scopes in `_meta` and on `handler.definitions` so a client can say why a call would fail. When a definition's existence is itself sensitive, it belongs on a second endpoint.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Thank you for following the naming conventions! 🙏 |
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.
📚 Description
Top of the stack, on #333. Depends on #330 for the verified claims it reads.
defineMcpTool,defineMcpResourceanddefineMcpPrompttakescopes.requireScopeswraps the handler eachbuild()pushes, reads the claims offevent.context.oauth, and throws a JSON-RPC-32003naming the scopes that were missing. Scopes come fromscope(space-delimited, RFC 6749) andscp(a string or an array, as Okta and Entra ID send it);permissionsis Auth0-specific and deliberately left out.It fails closed: a definition that declares scopes on an endpoint with no OAuth has nothing to satisfy them, so every call is refused.
The gate is on the call, not the listing, and that is the engine's order rather than a preference. h3-mcp resolves a handler's options — where the
X-MCP-Toolsfilter lives — beforecheckAuthpopulatesevent.context.oauth, and that resolution is synchronous, so no JWT check fits there. Thedecorateplugin hook does run after auth, but only fromhandleModernPost, so filtering there would leave legacy requests ungated whileeraisdual. A scoped definition therefore stays visible intools/list, with its scopes in_metaand onhandler.definitionsso a client can say why a call would fail. When a definition's existence is itself sensitive, it belongs on a second endpoint. Hiding listings needs a post-auth hook covering both eras upstream.One wrinkle worth knowing when reading the tests: the
403only reaches the wire on the modern revision. A legacy request gets the same JSON-RPC error inside a200stream, as it does for every error — so the tests assert on the code, not the status, unless they pin an era.📝 Checklist