MCP auth + config lifecycle: rotating credentials, interactive OAuth, validated config reload
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Split out from #391 (claim 5). This is a feature-boundary item, recommended to be split further into the three tracks at the bottom.
Claim
Mecatl currently supports static MCP bearer tokens. A general-purpose UI must separately implement protected-resource discovery, authorization-server metadata, dynamic client registration, PKCE, token exchange, and refresh. #342 covers refreshing short-lived bearer credentials and is related; interactive MCP OAuth discovery/authorization is complementary rather than a duplicate. Validated configuration reload would also avoid restarting the daemon — and disrupting sessions or memory-only credentials — when changing router or guardrail policy.
Investigation (verified against the codebase)
Verdict: all three factual premises confirmed.
1. Static-bearer-only MCP auth
The sole MCP auth seam is ServerConfig.Headers (internal/adapter/mcp/mcp.go:73-75) → static headerRoundTripper (:152-168, origin-scoped against redirect credential leak), snapshotted once in Connect (:272-291) and immutable for the Server's lifetime — reconnects reuse the same static headers. MCP_<NAME>_TOKEN is read once at flag parse (internal/cliconfig/mcpserver.go:85-91). Repo-wide grep for OAuth/PKCE/DCR/protected-resource across *.go finds zero MCP-auth code.
Notably, the vendored go-sdk v1.6.1 does ship the full client OAuth stack (auth.OAuthHandler auth/client.go:24, DynamicClientRegistrationConfig, auth-code+PKCE authorization_code.go:33-76) — mecatl imports none of it.
2. No config reload
Zero fsnotify/SIGHUP/reload hits; only SIGINT/SIGTERM graceful shutdown (cmd/mecated/main.go:720). All folds run once in app.Build; a settings.yaml change requires a daemon restart. Not inventoried or deferred anywhere in ADR 0027 Lists 1-2 or PRODUCTION-READINESS — never on the roadmap.
3. #342 scope
gh could not fetch #342's text in the investigation environment (org SAML), so this is a flagged caveat — but there is strong in-repo corroboration: ADR 0082 documents the exact pain (a per-run short-lived bearer presented via MCP_<NAME>_TOKEN, shared process-wide). The claim's characterization of #342 as the short-lived-bearer-refresh seam is consistent with everything findable, and its "complementary rather than a duplicate" framing is fair.
Recommended split into three tracks
(a) Per-request credential callback (~1 day)
Replace the static-header snapshot with a HeadersFunc/TokenSource on ServerConfig consulted inside headerRoundTripper.RoundTrip, so short-lived bearers rotate without a daemon restart. Unblocks the #342 scenario today. Note: the current RoundTripper holds a copy (mcp.go:273-290), so a refresh-capable one must read through a TokenSource-like interface per request; refreshed tokens then propagate to ADR 0056 redials automatically.
(b) Interactive MCP OAuth — its own ADR (multi-day-to-weeks)
The protocol leg is SDK-given; the real design question is UX: the daemon is headless and cannot own a browser dance, so the authorization URL + callback must cross the client wire (new proto/HTTP surface or an out-of-band channel), with token persistence in authfile (the unmerged openai-oauth-login branch's OAuthEntry shape is the natural schema). Constraints: tokens never logged/projected; a token cache is a new ADR 0027 List 1 row; 401→refresh→retry must not blindly consume the one-bounded-reconnect-per-call budget; envscrub's *_TOKEN denylist means a refresh flow must not rely on reading env late in a scrubbed runner context.
(c) Validated config reload (days)
SIGHUP or POST /v1/admin/reload re-running the pure folds into a new immutable Config with an atomic swap — but scoped honestly to the policy/config plane (permission rules, router taxonomy, guardrail specs, slots). Connections (MCP manager) and live engines are not safely reloadable; closures built at Build capture Config by value (e.g. buildModelRouterTask build.go:5153), so reload needs an indirection layer or rebuild-and-swap of those closures. Deny-dominance/tighten-only invariants must survive: a reloaded config must fold through the same foldOperator* + audience machinery, never bypass.
Constraints (all three)
- Secrets discipline (AGENTS.md Headers invariant).
- ADR 0027 List 1 rows for any outlives-a-call state (token cache, reload swap indirection).
- Any new client wire surface → proto change →
task generate. - Same-PR
user-docs/updates (user-docs/what-you-get/mcp-client.md:29-34documents the current bearer convention).
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 with internal/adapter/mcp/mcp.go:73-75, 152-168, and 272-291 for the current static-header lifecycle, then inspect cmd/mecated/main.go:720 and the referenced build.go closure. Choose one of the three proposed tracks rather than the whole boundary, and define tests for credential rotation, OAuth flow, or policy reload. Done includes preserving the AGENTS.md secrets invariant, updating user-docs/what-you-get/mcp-client.md, and running task generate if a client wire surface changes.
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
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100