Repository navigation
feat(mcp): lazy auth-header deferral in proxy mode - #1
Merged
Merged
Conversation
Introduce HeaderResolver type and extend NewTransport to accept a static-headers + lazy-auth-resolver pair. Each Transport struct gains staticHeaders, resolveAuth, authMu and cachedAuth fields plus effectiveHeaders/ensureAuth helpers. Header reads route through effectiveHeaders so a later commit can flip the auth-retry path on without touching the request-building code again. Behaviour is unchanged: all callers pass nil for resolveAuth so cachedAuth is never populated and effectiveHeaders returns staticHeaders only.
StreamableHTTP.Send now detects 401/403 responses and, if a HeaderResolver is configured and no auth has been resolved yet, invokes the resolver and retries the request once with the freshly cached auth headers. Subsequent requests reuse the cached headers (sticky authenticated state). 401/403 returned after auth is already present is propagated as a genuine failure. runGetStream silently closes on 401/403 (same as 404/405): the optional server-notifications GET never drives auth resolution; the next POST does.
SSETransport.Connect now retries the GET /sse handshake once with resolved auth headers if the unauthenticated GET returns 401/403. SSETransport.Send retries POST /messages the same way. When auth is resolved late (server allowed GET without auth, then demanded it on POST) the long-lived GET stream is NOT reopened with auth. Documented as a known limitation: servers that filter server-initiated notifications on auth should be configured with --transport http instead of --transport sse (or auto).
RunProxy now splits cfg.Headers into static (sent on every request from the start) and templated (resolved on demand from the vault). buildAuthResolver builds a HeaderResolver closure that the transport invokes the first time the remote MCP server returns 401/403. Servers that authorise the MCP initialize handshake without auth (a common pattern) no longer trigger a vault prompt at all until the AI client invokes a tool that requires authorization. Also tightens runLoop's shutdown so the server-message forwarder exits once stdin EOFs (or any send error fires), draining any buffered responses before closing the transport. Without this the new end-to-end proxy tests against a real httptest server would block indefinitely because real Transport implementations never close msgCh on their own.
Document that templated headers (those referencing the vault) are held back until the remote MCP server responds 401/403, and that servers needing auth on long-lived SSE GET streams should use --transport http to avoid the documented limitation.
Extend the MCP wrapper proxy-mode bullet with the auth state machine (static headers eager, templated headers resolved on 401/403, sticky authenticated state). Document the SSE GET reopen limitation and the --transport http workaround.
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.
Summary
In
locksmith mcp run --url(proxy mode), vault-resolved HTTP headers are now sent only after the remote MCP server responds401/403. Servers that authorise the MCPinitializehandshake without auth (a common pattern - capability negotiation usually does not require credentials) no longer trigger a vault prompt at all for that connection.This builds on the lazy-fetch + session-refresh work already on
developand goes one step further: even the first MCP request from the AI client (initialize) no longer pays the auth cost up front.Behaviour
Configured headers split into two groups based on the template syntax:
{token, e.g.User-Agent: MyClient): sent on every request from the very first one.{key:...}or{vault:... path:...}): resolved and attached only after the server signals it needs them.State machine:
StreamableHTTPandSSETransportboth retry on401/403.AutoTransportdelegates to its inner transport; the 404/405-driven fallback path is orthogonal to auth-retry.What changed
b99ca82refactor(mcp): plumb HeaderResolver through Transport(no behaviour change)8fecc77feat(mcp): Streamable HTTP transport retries with auth on 401/4037000673feat(mcp): SSE transport retries with auth on 401/4031d6d64cfeat(mcp): defer templated proxy headers to first 401/403(proxy.go wiring + E2E tests +runLoopshutdown hardening)a82fd42docs: describe lazy auth-header deferral for proxy mode(README + configuration.md)ff4da22docs(architecture): describe auth-header deferral and SSE GET limitc09e163docs(changelog): record lazy auth-header deferral in proxy modeSide fix in
1d6d64c:runLoopnow closes the transport explicitly on stdin EOF / send error and drains pending server messages. RealStreamableHTTPtransports never closemsgChon their own, so the prior loop would deadlock when stdin EOFs against a non-mock transport. The new end-to-end tests inproxy_test.goexposed it.Known limitations
/ssewithout credentials but then demands them onPOST /messages, the existing GET is left as-is and notifications on it may stop arriving. Workaround: configure such servers with--transport http(Streamable HTTP has no long-lived GET).exec; lazy auth is fundamentally not possible without changing the MCP server contract. The existing lazy-fetch (resolve on first stdin line) is as deep as local mode can go.errorpayload signalling auth must surface 401/403 instead.Configuration
No new flags, no new config fields. The deferral kicks in automatically for any header whose template contains a
{token. Plain static headers behave as before.Test plan
make lintclean (golangci-lint v2 strict ruleset).make test-raceclean across the workspace.make test-coverageshows >= 90% per package.~/.config/locksmith/config.yamlwith a templatedAuthorizationheader against a real backend; observe that no vault prompt fires onlocksmith mcp run --server <name>startup, only when the server returns 401/403.