diff --git a/apps/shared/src/gateway-contract.generated.ts b/apps/shared/src/gateway-contract.generated.ts index d301ccb748..e3b1baa0f9 100644 --- a/apps/shared/src/gateway-contract.generated.ts +++ b/apps/shared/src/gateway-contract.generated.ts @@ -3610,6 +3610,7 @@ export interface McpOauthCallbackParams { code?: string | null state?: string | null error?: string | null + iss?: string | null } export interface McpOauthCallbackResult { ok: boolean diff --git a/apps/shared/src/gateway-contract.openrpc.json b/apps/shared/src/gateway-contract.openrpc.json index c92821e7bc..76bad2cddc 100644 --- a/apps/shared/src/gateway-contract.openrpc.json +++ b/apps/shared/src/gateway-contract.openrpc.json @@ -13814,6 +13814,18 @@ ], "default": null, "title": "Error" + }, + "iss": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Iss" } }, "required": [ diff --git a/tests/hermes_cli/test_mcp_dashboard_oauth.py b/tests/hermes_cli/test_mcp_dashboard_oauth.py index 91f30ec4f0..c4df8eeeab 100644 --- a/tests/hermes_cli/test_mcp_dashboard_oauth.py +++ b/tests/hermes_cli/test_mcp_dashboard_oauth.py @@ -85,7 +85,7 @@ def test_hosted_callback_bypasses_gated_cookie_auth(monkeypatch): ) assert response.status_code == 200 - assert flow._callback == ("abc", "expected") + assert flow._callback == ("abc", "expected", None) def test_hosted_auth_allows_same_server_name_in_different_profiles(tmp_path, monkeypatch): diff --git a/tests/tui_gateway/test_mcp_oauth_client_callback.py b/tests/tui_gateway/test_mcp_oauth_client_callback.py index 6ca187ec10..17e99c13b3 100644 --- a/tests/tui_gateway/test_mcp_oauth_client_callback.py +++ b/tests/tui_gateway/test_mcp_oauth_client_callback.py @@ -201,6 +201,23 @@ def test_deliver_callback_forwards_iss(): assert flow._callback == ("abc", "s3cr3tstate", "https://as.example.com") +def test_oauth_callback_rpc_relays_iss(): + """The gateway ``mcp.servers.oauth.callback`` RPC accepts ``iss`` under the extra=forbid contract + and forwards it to the flow; the desktop renderer always sends the key (possibly null).""" + import tui_gateway.server as srv + from tui_gateway.contracts import registry as contracts + + flow = _make_session() + contract = contracts.METHODS["mcp.servers.oauth.callback"] + params = {"session_id": "sess-relay-1", "name": "hosp", "code": "abc", "state": "s3cr3tstate", + "iss": "https://as.example.com"} + params, problem = contracts.validate_params(contract, params) + assert problem is None + out = srv._methods["mcp.servers.oauth.callback"](1, params) + assert out["result"]["ok"] is True + assert flow._callback == ("abc", "s3cr3tstate", "https://as.example.com") + + def test_loopback_listener_forwards_iss(): """The gateway-hosted loopback listener parses ``iss`` off the redirect rather than dropping it.""" import urllib.request diff --git a/tui_gateway/contracts/tools_mcp_plugins.py b/tui_gateway/contracts/tools_mcp_plugins.py index e648867f89..7e6ef383e8 100644 --- a/tui_gateway/contracts/tools_mcp_plugins.py +++ b/tui_gateway/contracts/tools_mcp_plugins.py @@ -530,6 +530,8 @@ class McpOauthCallbackParams(McpOauthFlowParams): code: str | None = None state: str | None = None error: str | None = None + # RFC 9207 issuer; extra="forbid" would otherwise 4000 the desktop relay that always sends it. + iss: str | None = None class McpOauthCallbackResult(Result): diff --git a/tui_gateway/methods_tools.py b/tui_gateway/methods_tools.py index 044598c46f..80776f3a49 100644 --- a/tui_gateway/methods_tools.py +++ b/tui_gateway/methods_tools.py @@ -1369,7 +1369,8 @@ def _(rid, params: dict) -> dict: code, state, error, iss = (str(params.get(k) or "") or None for k in ("code", "state", "error", "iss")) deliver = _tools_mod("tui_gateway.mcp_oauth_sessions").deliver_callback_flow return _ok(rid, deliver( - _str_arg(params, "session_id"), _str_arg(params, "name"), code=code, state=state, error=error)) + _str_arg(params, "session_id"), _str_arg(params, "name"), code=code, state=state, error=error, + iss=iss)) # ─── Plugins ─────────────────────────────────────────────────────────────────