[Bug] Streamable HTTP MCP server: OAuth discovery drops the authorization server's path prefix
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 22
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
Summary
For a Streamable HTTP MCP server whose OAuth authorization server lives under a sub-path (not at the host root), ZCode's OAuth flow opens an authorization URL that has the authorization server's path prefix stripped, replacing the correct authorization_endpoint with a host-root-relative /authorize. Token and registration endpoints are affected the same way.
Result: OAuth login fails because the browser opens e.g. `https://example.com/authorize\` instead of the correct `https://example.com/app/oauth/authorize\`.
Other MCP clients (e.g. the ones bundled with `hermes` and `codex`) correctly follow the discovery chain and open the right URL, so the issue appears specific to ZCode's client implementation.
Environment
- ZCode version: (latest, 3.x kernel)
- OS: macOS (also reproducible on Linux)
- MCP transport: Streamable HTTP (http)
Configuration
Minimal MCP config reproducing the issue (server under a sub-path):
```json
{
"mcp": {
"servers": {
"subpath-server": {
"type": "http",
"url": "https://example.com/app/mcp"
}
}
}
}
```
The server config itself is correct — the `url` points at the real MCP endpoint including the `/app` prefix. The bug is downstream, in OAuth discovery/URL construction.
Reproduction
The server publishes a standard RFC 8414 / RFC 9728 discovery chain. All responses below are observed (paths shown relative to the host):
-
`GET https://example.com/app/mcp\` → `401`, header:
```
www-authenticate: Bearer resource_metadata="https://example.com/app/.well-known/oauth-protected-resource\"
``` -
`GET https://example.com/app/.well-known/oauth-protected-resource\` →
```json
{
"resource": "https://example.com/app/mcp\",
"authorization_servers": ["https://example.com/app"],
"bearer_methods_supported": ["header"]
}
``` -
`GET https://example.com/app/.well-known/oauth-authorization-server\` →
```json
{
"issuer": "https://example.com/app\",
"authorization_endpoint": "https://example.com/app/oauth/authorize\",
"token_endpoint": "https://example.com/app/oauth/token\",
"registration_endpoint": "https://example.com/app/oauth/register\",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256", "plain"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["kmt"]
}
```
This chain is internally consistent and is exactly what other clients consume successfully.
Expected behavior
ZCode follows the discovery chain (resource metadata → `authorization_servers` → `/.well-known/oauth-authorization-server`) and uses the absolute URLs returned in the authorization-server metadata as-is, without rewriting them.
Concretely, the authorization URL opened in the browser should be:
`https://example.com/app/oauth/authorize?client_id=...&...\`
Actual behavior
ZCode opens:
`https://example.com/authorize?client_id=...&...\`
i.e. the path prefix of the authorization server (`/app`) — and the `/oauth` segment — are discarded, and a literal `/authorize` is appended to the host root. The same stripping is expected to break `/token` and `/register` (would be requested at `/token` and `/register` instead of `/app/oauth/token` and `/app/oauth/register`).
Analysis / likely cause
The pattern (host root + literal `/authorize`, with both `/app` and `/oauth` dropped) is consistent with the client not following RFC 8414 discovery and instead normalizing the issuer/resource to the host root and hard-coding the endpoint suffixes. When the authorization server's `issuer` already contains a path prefix (here `/app`), that prefix is lost.
A correct implementation should:
- Start from the `resource_metadata` URL in the `401` `WWW-Authenticate` header (RFC 9728).
- Read `authorization_servers` from the protected-resource metadata.
- Fetch `/.well-known/oauth-authorization-server` for each (RFC 8414), resolving the `.well-known` segment relative to the issuer's path, not the host root. (For `https://example.com/app\`, the metadata URL is `https://example.com/app/.well-known/oauth-authorization-server\`, per RFC 8414 §3.)
- Use the absolute `authorization_endpoint` / `token_endpoint` / `registration_endpoint` values from the metadata verbatim.
Workaround
Server-side: add redirect rules mapping the host-root endpoints back to the real ones, e.g. `/authorize` → `/app/oauth/authorize`. This unblocks login but is purely a workaround; the client should be fixed to honor the discovery metadata.
Impact
Any MCP server published under a sub-path with its own OAuth 2.1 authorization server (common when multiple apps share one host) cannot complete OAuth in ZCode without a server-side redirect hack.
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 at the Streamable HTTP client's OAuth discovery flow and reproduce the issue with the provided subpath-server configuration and discovery responses. Trace how the resource metadata, authorization server issuer, and endpoint URLs are handled. Done means the browser uses the returned /app/oauth/authorize URL and token and registration requests preserve their /app/oauth/ paths.
Written by the indexing model from the issue text.
Assessment
- Domain
- authentication
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100