mecatl: should MCP broker OAuth authorization be scoped to a principal/lineage instead of strictly per-session?
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Summary
MCP broker OAuth authorization/enrollment state (ADR 0311) is keyed strictly
1:1 by mecatl session ID. Any operation that creates a new session ID for what
is conceptually the same principal continuing the same work — most notably
switching the active model/provider mid-conversation via ForkSession, but
also ClearSession — currently forces that new session to start with zero
broker authorization, even though nothing about the user's identity or
already-granted upstream consent has changed. The user has to re-run OAuth
enrollment for every protected MCP backend after every model switch.
This issue is about whether that scoping choice should be revisited, not about
the currently-broken mechanics of fork/clear in broker mode (that's a
separate, already-filed bug: #1351 — ForkSession/ClearSession don't even
mint a fresh binding correctly today, let alone consider retaining the old
one). Once #1351 is fixed, forking will work, but every fork will still cold-
start unauthorized. This issue asks: should it?
Where the scoping decision currently lives
internal/adapter/mcpbroker/runtime.go,Runtime.AttachSession(~line 273-275):
"creates or reattaches to logical state keyed by the canonical mecatl session
ID." The map isr.sessions[id]— one broker logical session per mecatl
session ID, no other identity axis exists at this layer.docs/adr/0311-per-upstream-mcp-broker-oauth-grants.md: explicitly scopes
itself as "session-scoped MCP broker OAuth authorization" and states mecatl
"does not interpret that credential as an upstream grant... does not
implement... grant copying." ToolHive alone owns the authorization chain;
mecatl is deliberately not supposed to assert on its own that a credential
valid in one context is valid in another.session.ExternalBinding(engine/session/session.go:18) is the opaque
handle mecatl persists to reattach to that same session-ID-keyed logical
state (e.g. across a process restart of the same session).
Why this is worth reconsidering
A model-switch fork and the source session it forked from share the same
authenticated principal and the same conversation lineage — they are not two
different users, and not a scenario where reusing consent would cross a trust
boundary. Forcing a full re-authorization dance on every model switch is a
real, user-visible friction cost with no corresponding security benefit that I
can identify: the thing OAuth grants are actually about (a principal's consent
for a backend) hasn't changed; only the LLM provider/model selection has.
Evidence that "reuse" doesn't have to mean "copy"
Runtime.AttachSession already has first-class support for multiple
independent attachments to one logical session — this is not something that
would need to be invented:
- The reattach branch (
runtime.go:302-309) explicitly handles a second
attachment to an existing logical session, and already knows how to hand a
freshly-attached handle an already-completed enrollment
(runtime.go:323-330,installCompletedEnrollment) without any new
authorization round-trip — this is exactly how a restarted process reattaches
to a still-live broker session today. Attachment.Close/Attachment.Abort(runtime.go:395-465) already
distinguish a "creator" attachment from a "reattacher" one: aborting or
closing a reattacher's handle never tears down the shared logical session
underneath a sibling attachment that's still using it.
So a forked session picking up its source's already-completed enrollment would
not require mecatl to assert or copy anything — it would be the same
ToolHive-verified state, opened by a second handle, using a mechanism the
runtime already relies on for a different case (restart-reattach). That reframes
the question away from "should mecatl duplicate an authorization decision"
(clearly wrong, per ADR 0311) toward "should two deliberately parallel
mecatl sessions be allowed to hold concurrent attachments to the same broker
logical incarnation" (not yet decided either way).
What stands in the way — this is a real design gap, not a small patch
mecatl's own session lifecycle assumes 1:1, not N:1, between a mecatl session
and a broker logical incarnation:
Service.DeleteSession(internal/adapter/server/service.go:3440) keys the
broker-side delete by the mecatl session's own ID. If two sessions shared one
broker binding, deleting either one today would either miss the shared state
entirely, or destroy it out from under the sibling still using it — e.g.
deleting your old pre-switch session would silently deauthorize the session
you switched to. There is no reference-counting or "detach without delete"
concept today.- Snapshot/rehydration and GC likewise assume
ExternalBindinguniquely and
permanently identifies exactly one mecatl session's own broker state. - ADR 0311 explicitly frames the current design as session-scoped; widening
that is a change to a stated architectural decision, not a bugfix — by this
repo's own conventions (see AGENTS.md's ADR lifecycle rules) that means a
new or superseding ADR, not a quiet code change.
Possible high-level directions (not evaluated in depth — for the next investigator)
These are sketched at a conceptual level only; no implementation approach is
being proposed or endorsed here.
- Do nothing / keep strictly session-scoped. Accept the re-authorization
cost as the price of strict per-session isolation. Simplest, but leaves the
UX friction in place indefinitely, and every future session-forking feature
(not just model-switch) inherits the same cost. - Introduce a separate "authorization lineage" identity, distinct from the
mecatl session ID, that the broker keys on instead. E.g. a stable id
established once per top-level conversation and carried forward through
fork/clear (but NOT through genuinely new/unrelated sessions). The broker's
AttachSessionwould key on this lineage id; mecatl session IDs remain the
unit of LLM/provider/history identity as they are today. This directly
solves the reattachment case above, but requires solving the N:1
delete/GC/lifecycle-ownership problem explicitly (e.g. explicit
detach-vs-delete semantics, or last-referencing-session-triggers-delete). - Keep binding == session ID, but let fork/clear explicitly reuse the
source's existing binding value instead of minting a new one, accepting
that the mecatl session ID and the broker binding are no longer always
equal for a forked session. Smaller conceptually than (2) but has the same
underlying delete/lifecycle problem, just pushed onto the binding field's
uniqueness assumption instead of a new identity type. - Leave grants session-scoped, but shrink the friction another way — e.g.
verify whether ToolHive's own upstream-token storage (which already
resolves a canonical user identity distinct from the mecatl session ID,
permaybeCarryForwardRefreshTokenin the vendored
github.com/stacklok/toolhive/pkg/authserver/server/handlers/callback.go)
already makes a second interactive authorization for the same real user
fast (e.g. instant redirect through an active upstream IdP session, no new
consent screen) even without any mecatl-level state sharing. If so, part of
the perceived problem may already be substantially mitigated by ToolHive's
own behavior, and the real gap is smaller than it first appears. Worth
confirming empirically before investing in (2) or (3).
Explicitly out of scope for a fix here
- Any change to who owns the OAuth chain (ToolHive continues to own it per
ADR 0311 — this issue is only about how many mecatl sessions may reference
one already-completed authorization, not about mecatl acquiring or
interpreting credentials itself). - The already-filed, purely mechanical bug in #1351 (fork/clear currently
fails outright in broker mode; fix that first regardless of this issue's
outcome).
Ask
Investigate which of the directions above (or another not listed) is the right
one, whether it needs a new ADR (my read: yes, given ADR 0311's explicit
"session-scoped" framing), and produce a recommendation — this issue
deliberately stops short of proposing one so that analysis starts fresh rather
than anchoring on my framing.
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
Read docs/adr/0311-per-upstream-mcp-broker-oauth-grants.md and the ADR lifecycle rules in AGENTS.md first. Trace Runtime.AttachSession in internal/adapter/mcpbroker/runtime.go alongside Service.DeleteSession in internal/adapter/server/service.go, then assess the proposed scoping options and ToolHive token behavior. Done means a recommendation on authorization scope and lifecycle ownership, with a new or superseding ADR if the decision changes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100