Support a pinned OAuth callback port for MCP servers - #491
Draft
samcx wants to merge 5 commits into
Draft
Conversation
Interactive MCP authorization bound the loopback callback listener to port 0, so every attempt advertised a different redirect_uri. Providers that require a pre-registered redirect could never be authorized, since no registration can match a port that changes per run. Add an optional oauth.callback_port to the MCP server config. When set, the listener binds that port and advertises http://localhost:<port>/callback, matching the host form providers register. Without it the ephemeral 127.0.0.1 behavior is unchanged.
Allow configured callback ports to be reused after authorization without sharing active listeners. Continue with IPv4 when IPv6 loopback is unavailable, and create callback sockets with close-on-exec atomically where supported.
fazxes
force-pushed
the
samcx/mcp-oauth-callback-port
branch
from
August 29, 2026 15:27
8d82d94 to
e2f6863
Compare
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
oauth.callback_portto MCP server confighttp://localhost:<port>/callbackwhen a port is pinned, matching the host form providers registerProblem
authorizeInteractivebinds the loopback callback listener to port0, so the OS assigns a different port on every attempt and the advertisedredirect_urichanges each run:Providers that require a pre-registered redirect URI can never be authorized, because no registration can match a port that changes per run. Slack's hosted MCP server (
https://mcp.slack.com/mcp) is the case that surfaced this: it does not support Dynamic Client Registration, so the client must supply aclient_idwhose redirect URI is already registered.There is a second, quieter mismatch. Registered loopback redirects are conventionally written as
localhost, and providers compare redirect URIs as exact strings, so emitting127.0.0.1fails to match even when the port is correct.Setting
oauth.client_idalone already clearsClientRegistrationUnavailable, so today the flow gets past discovery and then fails at the provider's redirect validation with no way to proceed.Reproduction
{ "mcp": { "slack": { "type": "http", "url": "https://mcp.slack.com/mcp", "enabled": true, "oauth": { "client_id": "<a registered Slack app client id>" } } } }fx mcp auth slackopens an authorize URL whoseredirect_uriis a fresh ephemeral port, which the provider rejects.Fix
When
callback_portis set the listener binds it and the redirect becomeshttp://localhost:3118/callback. When it is absent, behavior is byte-for-byte unchanged: ephemeral port,127.0.0.1host. Binding still happens on127.0.0.1; only the advertised string changes.isLoopbackHostalready acceptslocalhost,127.0.0.1, and[::1], so nothing downstream shifts.A pinned port that is already in use now fails as
McpCallbackPortUnavailablerather than a bare listen error, since that is a routine and recoverable user situation.Ports are validated at config parse time and must be 1-65535.
This mirrors prior art: opencode exposes
callbackPortandredirectUri, and Claude Code pins3118.Verification
zig buildclean,zig fmt src/cleanzig build test: 8747/8755 pass, +3 from this change. The 6 failures are pre-existing and environmental on this machine (iTerm2 shell-integration escapes leaking into captured command output incommand_runner/tool_runtime/builtins.context); the identical 6 fail on an unmodified checkout, and none touch the files in this diff../zig-out/bin/fx mcp auth slackagainst an isolated profile:with
callback_port: 3118with the key removed
The pinned run produced a complete authorize URL carrying the configured
client_id, PKCES256, theresourceparameter, and all scopes advertised by the server's protected-resource metadata.