Skip to content

fix(mcp): add consistent callback_port for OAuth MCP servers - #4843

Open
jpelletier1 wants to merge 1 commit into
mainfrom
fix/oauth-mcp-consistent-callback-port
Open

fix(mcp): add consistent callback_port for OAuth MCP servers#4843
jpelletier1 wants to merge 1 commit into
mainfrom
fix/oauth-mcp-consistent-callback-port

Conversation

@jpelletier1

@jpelletier1 jpelletier1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

HUMAN:


AGENT:

Why

OAuth-based MCP server connections were failing to refresh expired tokens, specifically affecting OAuth providers like Atlassian Rovo and GitLab that require pre-registered redirect URIs. The root cause was that FastMCP's OAuth client was using a random port on each authorization attempt, causing "invalid callback URL" errors because the port didn't match the pre-registered redirect URI.

Summary

  • Added callback_port field to MCPOAuthAuthentication with default value of 8765 to ensure consistent redirect URIs across sessions
  • Updated _oauth_auth_from_authentication_config() to pass callback_port to FastMCP's OAuth constructor
  • Added comprehensive test coverage for callback_port configuration (explicit port, default port, and null/random port)

REST API contract changes

Compared with base OpenAPI bf8a121d8a3e for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -1844,0 +1845 @@
+schema MCPOAuthAuthentication-Input property callback_port optional schema=anyOf=[type="integer",type="null"] default=8765
@@ -1853,0 +1855 @@
+schema MCPOAuthAuthentication-Output property callback_port optional schema=anyOf=[type="integer",type="null"] default=8765

Issue Number

Fixes OpenHands/OpenHands#17077

How to Test

1. Run the test suite:

# Run all MCP OAuth tests
uv run pytest tests/agent_server/test_mcp_oauth_store.py -v

# Run the new callback_port test specifically
uv run pytest tests/agent_server/test_mcp_oauth_store.py::test_oauth_callback_port_configuration -v

# Run all MCP-related tests
uv run pytest tests/sdk/mcp/ tests/agent_server/test_mcp_oauth_store.py -v

All 139 MCP-related tests pass successfully.

2. Test with a real OAuth MCP server:

Configure an OAuth MCP server in your settings:

{
  "your-oauth-server": {
    "url": "https://your-provider.com/mcp",
    "auth": {
      "strategy": "oauth2",
      "authentication": {
        "type": "oauth",
        "client_id": "your-client-id",
        "client_secret": "your-client-secret",
        "scopes": ["read:data"],
        "callback_port": 8765
      }
    }
  }
}

Register your OAuth application with redirect URI: http://localhost:8765/callback

The OAuth flow should now work consistently without "invalid callback URL" errors.

3. Verify backward compatibility:

  • Existing configurations without callback_port will use the default (8765)
  • Setting callback_port: null still allows random ports (for development)
  • Custom ports work correctly when specified

Video/Screenshots

Test output showing all 139 MCP tests passing:

===================================================================================================================== test session starts ======================================================================================================================
platform linux -- Python 3.13.14, pytest-9.0.3, pluggy-1.6.0
collected 139 items

tests/sdk/mcp/test_create_mcp_tool.py ..................................... [ 28%]
tests/sdk/mcp/test_mcp_action_serialization.py ....... [ 33%]
tests/sdk/mcp/test_mcp_config_secrets.py .. [ 35%]
tests/sdk/mcp/test_mcp_nested_schema.py ............. [ 44%]
tests/sdk/mcp/test_mcp_observation.py .......... [ 51%]
tests/sdk/mcp/test_mcp_session_persistence.py ................ [ 62%]
tests/sdk/mcp/test_mcp_tool.py .............. [ 72%]
tests/sdk/mcp/test_mcp_tool_immutability.py ...... [ 77%]
tests/sdk/mcp/test_mcp_tool_kind_field.py .... [ 79%]
tests/sdk/mcp/test_mcp_tool_list_changed.py ................ [ 91%]
tests/sdk/mcp/test_mcp_tool_serialization.py ..... [ 94%]
tests/sdk/mcp/test_mcp_tool_validation.py ... [ 97%]
tests/sdk/mcp/test_stateful_mcp.py ... [ 99%]
tests/agent_server/test_mcp_oauth_store.py ..... [100%]

=============================================================================================================== 139 passed, 21 warnings in 8.65s ===============================================================================================================

Design Doc

See .pr/oauth-mcp-fix-summary.md for comprehensive documentation including:

  • Root cause analysis
  • Implementation details
  • Configuration examples for Atlassian Rovo and GitLab
  • Migration guide for existing users

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

Configuration Changes:

  • New optional field callback_port in MCPOAuthAuthentication (defaults to 8765)
  • Backward compatible: existing configurations work without changes
  • Users with OAuth providers requiring pre-registered redirect URIs should ensure their OAuth app is registered with http://localhost:8765/callback (or their custom port)

Token Expiry Handling:

  • The first reported issue (token expiry not loaded on init) was already fixed in FastMCP 3.2.0
  • The _initialize() method properly loads and restores token_expiry_time from storage
  • Token refresh mechanism works correctly with the current FastMCP version

Follow-ups:

  • Consider documenting the callback_port configuration in user-facing docs
  • May want to add a note about firewall/port forwarding requirements for the callback server

🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python-slim amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:e0b52e3-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-e0b52e3-python \
  ghcr.io/openhands/agent-server:e0b52e3-python

All tags pushed for this build

ghcr.io/openhands/agent-server:e0b52e3-golang-amd64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-golang-amd64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-golang-amd64
ghcr.io/openhands/agent-server:e0b52e3-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:e0b52e3-golang-arm64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-golang-arm64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-golang-arm64
ghcr.io/openhands/agent-server:e0b52e3-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:e0b52e3-java-amd64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-java-amd64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-java-amd64
ghcr.io/openhands/agent-server:e0b52e3-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:e0b52e3-java-arm64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-java-arm64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-java-arm64
ghcr.io/openhands/agent-server:e0b52e3-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:e0b52e3-python-amd64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-python-amd64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-python-amd64
ghcr.io/openhands/agent-server:e0b52e3-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:e0b52e3-python-arm64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-python-arm64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-python-arm64
ghcr.io/openhands/agent-server:e0b52e3-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:e0b52e3-python-slim-amd64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-python-slim-amd64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-python-slim-amd64
ghcr.io/openhands/agent-server:e0b52e3-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim-amd64
ghcr.io/openhands/agent-server:e0b52e3-python-slim-arm64
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-python-slim-arm64
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-python-slim-arm64
ghcr.io/openhands/agent-server:e0b52e3-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim-arm64
ghcr.io/openhands/agent-server:e0b52e3-golang
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-golang
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-golang
ghcr.io/openhands/agent-server:e0b52e3-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:e0b52e3-java
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-java
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-java
ghcr.io/openhands/agent-server:e0b52e3-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:e0b52e3-python-slim
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-python-slim
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-python-slim
ghcr.io/openhands/agent-server:e0b52e3-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-slim
ghcr.io/openhands/agent-server:e0b52e3-python
ghcr.io/openhands/agent-server:e0b52e3f0de79a962fab326d1119609afbdab739-python
ghcr.io/openhands/agent-server:fix-oauth-mcp-consistent-callback-port-python
ghcr.io/openhands/agent-server:e0b52e3-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., e0b52e3-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., e0b52e3-python-amd64) are also available if needed

OAuth providers like Atlassian Rovo and GitLab require pre-registered
redirect URIs. Previously, FastMCP OAuth client used a random port on
each authorization attempt, causing 'invalid callback URL' errors.

This fix:
- Adds callback_port field to MCPOAuthAuthentication (default: 8765)
- Passes callback_port to OAuth constructor in utils.py
- Adds test coverage for callback_port configuration

Users can now:
1. Register OAuth apps with http://localhost:8765/callback
2. Configure custom ports via callback_port if needed
3. Set callback_port=null for random port (development only)

Token expiry handling was already fixed in FastMCP 3.2.0.

Fixes: OpenHands/OpenHands#17077

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed when the PR is approved.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/mcp
   config.py3787281%93, 98–108, 122–124, 154, 180, 204, 334, 354, 396–399, 403–406, 410–412, 438–441, 445, 459, 463, 467, 472, 551, 553, 559, 571, 576–579, 585–587, 593, 630, 678–682, 684, 686–688, 690–693, 695–697, 704, 707, 709
   utils.py1511689%83–84, 133, 182–183, 186, 189, 247–248, 257–258, 303, 309–310, 388–389
TOTAL42031768582% 

@all-hands-bot

Copy link
Copy Markdown
Collaborator

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth-based MCP server connections do not refresh token (Atlassian Rovo & GitLab callback URL errors)

3 participants