modelcontextprotocol / modelcontextprotocol/go-sdk
proposal: auth: support SEP-2350 request-time scope challenges
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 5.1k
- Forks
- 543
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 37
Description
Is your feature request related to a problem? Please describe.
auth.RequireBearerToken can't produce a SEP-2350 conformant insufficient-scope challenge, for two separate reasons.
- Smaller/Possible Bug: On the 403 path the middleware builds a
WWW-Authenticateheader fromresource_metadataandscope, but never addserror="insufficient_scope". RFC 6750 §3.1 requires theerrorparam, and a client that switches on it sees an unlabelled challenge and can't tell a scope problem from anything else. - Larger/Structural:
RequireBearerTokenOptions.Scopesis fixed when the middleware is constructed, so a server has exactly one required scope set for every request. SEP-2350 is about advertising the scope a particular operation needs, so a low-privilege client can step up for the one tool that requires more. With a construction-time set there's no way to express "readfor most tools,admin-writefor this one", and thescope=the challenge advertises is the server-wide set rather than what the operation actually needed.
SEP-2350 is merged and labelled final (modelcontextprotocol/modelcontextprotocol#2350), so this might no longer be a tracking a draft?
Describe the solution you'd like
Two changes, separable, and would be happy to land only the first if the second needs more discussion.
-
Add
error="insufficient_scope"to the challenge on the 403 branch. Non-breaking in any way that matters, and imo it is a bug fix rather than a proposal. -
Let the required scopes vary per request. The smallest thing that could work is a callback on the options struct, leaving
Scopesas the static fallback:
type RequireBearerTokenOptions struct {
// ...
// ScopesForRequest returns the scopes required for this particular
// request. When nil, Scopes applies to every request.
ScopesForRequest func(*http.Request) []string
}
The middleware would use its result both for the containment check and for the scope= it advertises, so the challenge names what the operation needed rather than what the server requires in general.
One thing we want to deep dive a bit more is that RequireBearerToken is a generic net/http middleware and runs before the JSON-RPC body is dispatched, so a ScopesForRequest callback cannot see which tool is being called without the implementer parsing the body themselves. While this may be (coz it keeps the auth package free of MCP concepts and puts the choice in the caller's hands) if the preference is for the scope requirement to live next to the tool it guards then this is a different and larger design touching the mcp package. Would prefer to hear yall's thoughts on which which direction to take.
Describe alternatives you've considered
- A typed error from the handler. Let a tool handler return something like
auth.ErrInsufficientScope{Scopes: []string{"admin-write"}}that the HTTP layer maps to a 403 plus challenge. This puts the requirement next to the tool, which reads better, but it means the check happens after dispatch rather than in middleware, and it couplesmcptoauth. - Leave it to implementers. Callers can write their own middleware today. It works, but every implementer re-derives the RFC 6750 challenge format, and the SDK already owns that formatting for the 401 case, so the knowledge is half here already.
- Do nothing until the TypeScript SDK settles. Genuinely possible, see below.
Additional context
The main question I have before writing any code is whether we want to wait for the TypeScript SDK?modelcontextprotocol/typescript-sdk#1624 is the reference implementation for this, open since March, currently with changes requested. auth.go notes that RequireBearerToken is based on that SDK's bearerAuth.ts, so if you'd rather track their final API shape than diverge, that's a fair call. Id love to know now than send a PR prematurely.
Also from a conformance angle modelcontextprotocol/conformance#481 adds a server-side scope-challenge scenario covering tools/call, static and templated resources/read, and prompts/get. When it merges, the Go SDK will either fail it or need a new exclusion line in conformance/baseline.yml alongside the DPoP and WIF ones. IMO doing this now keeps it ready then.
Some prior-art - I've implemented the equivalent in another Go MCP library (panyam/mcpkit#1353, request-time challenges across all primitives) and it passes that scenario 17/17 with no warnings. So I have a design that's been through the conformance suite and I'm happy to write the PR here. Just want to make sure the extension point/design first with yall.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading auth.go, especially RequireBearerToken and RequireBearerTokenOptions, then review the SEP-2350 and RFC 6750 requirements described here. Check conformance#481 and conformance/baseline.yml to understand the expected server-side scenarios; done requires an agreed request-time scope design and conformance coverage without an unjustified baseline exclusion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100