Skip to content

[TT-17137] Support remote MCPs as upstream — routing fix + auto-mirror PRM + OAuth proxy#8181

Merged
lghiur merged 10 commits into
masterfrom
TT-17137-user-should-be-easily-able-to-set-remote-mcp-as-upstream
May 7, 2026
Merged

[TT-17137] Support remote MCPs as upstream — routing fix + auto-mirror PRM + OAuth proxy#8181
lghiur merged 10 commits into
masterfrom
TT-17137-user-should-be-easily-able-to-set-remote-mcp-as-upstream

Conversation

@andrei-tyk

@andrei-tyk andrei-tyk commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Make Tyk able to put a transparent reverse proxy in front of any spec-compliant remote MCP server (Atlassian Rovo, Notion, etc.) so MCP clients (Roo, Claude Desktop, anything that uses mcp-remote) can complete the OAuth dance through the gateway with zero per-upstream knobs. Three commits, each fixing a distinct failure point along the wire.

The minimal MCP API definition is now just listen path + upstream:

{
  "openapi": "3.0.3",
  "info": {"title": "jira", "version": "1.0.0"},
  "paths": {"/": {"get": {...}, "post": {...}}},
  "x-tyk-api-gateway": {
    "info": {"id": "jira-mcp-remote", "name": "jira", "state": {"active": true}},
    "server": {"listenPath": {"value": "/jira/", "strip": true}},
    "upstream": {"url": "https://mcp.atlassian.com/v1/mcp/authv2"}
  }
}

POST that to /tyk/mcps. Mirror mode auto-engages — no authentication block, no protectedResourceMetadata block. The full OAuth dance completes end-to-end through Tyk.

What was broken (three failure modes, in order)

  1. Routing: when the upstream URL had a path component (e.g. /v1/mcp/authv2), .well-known/* discovery probes were prefixed with that path and 404'd — well-known endpoints live at the host root.
  2. RFC 9728 §3.3 origin mismatch: even with routing fixed, transparent-proxying the upstream's PRM doc gives a resource field that doesn't match the URL the client connected to — strict clients (mcp-remote, Roo) refuse to authenticate.
  3. RFC 8707 invalid_target: even with PRM rewritten, the client sends the gateway URL as the resource parameter to the upstream's authorize endpoint. Strict authorization servers (Notion) only accept the upstream URL and reject with invalid_target.

What we built

  • TT-17137 routing fix (gateway/reverse_proxy.go): MCP-aware host-root branch in the reverse-proxy director routes well-known discovery probes (RFC 9728 / RFC 8414 / OIDC) to the upstream host root, ignoring the upstream URL's path.
  • PRM mirror mode (apidef/oas/authentication.go, gateway/mw_protected_resource.go, gateway/api_loader.go): new mode: "mirror" fetches the upstream's PRM doc, rewrites resource to the gateway URL, caches with TTL, auto-serves at the path-suffix URL clients actually probe (<gateway>/.well-known/oauth-protected-resource<listen-path>).
  • OAuth-flow proxy (gateway/mcp_oauth_proxy.go): mirror mode redirects authorization_servers at a per-API Tyk URL. Tyk serves a synthesised AS metadata document with authorization_endpoint/token_endpoint pointing at itself, then 302-redirects the authorize request and forwards token POSTs after rewriting the resource parameter from gateway URL to upstream URL. Reconciles RFC 9728 §3.3 vs. RFC 8707 on the wire.
  • Auto-mirror default: EffectiveMode(isMCP) resolves empty mode by configuration shape — Resource set → static (back-compat); Resource empty + MCP → mirror; everything else → static. OAS.ValidateForMCP threads MCP context through OAS-level validation so empty configs don't get rejected with "resource is required". GetPRMConfig synthesises a default for MCP APIs without an explicit PRM block, so the runtime plumbing engages with zero opt-in.
  • WWW-Authenticate rewriter (augmentMCPWWWAuthenticate): replaces (not appends) the resource_metadata= parameter on upstream 401s so clients are pointed at the gateway's PRM endpoint. Uses the inbound request's scheme/host (not the rewritten outbound).

Validated end-to-end

  • Atlassian Rovo (https://mcp.atlassian.com/v1/mcp/authv2) — Roo browser flow completes, tools list returned.
  • Notion (https://mcp.notion.com/mcp) — same. RFC 8707 invalid_target resolved by the AS proxy's resource parameter rewrite.

Tests

  • Unit: apidef/oas/authentication_test.go (Validate, EffectiveMode, IsMirrorMode), internal/mcp/discovery_test.go (host-root prefix matching), internal/mcp/prm_cache_test.go (TTL cache + URL derivation + upstream PRM fetch).
  • Integration: gateway/reverse_proxy_test.go::TestReverseProxyMCPHostRootDiscovery (routing fix; non-MCP regression guard), gateway/mw_protected_resource_test.go::TestPRMMirrorMode_SuffixRoute (PRM serving + rewrite), TestPRMMirrorMode_OAuthProxy (AS metadata rewrite, authorize 302, token forward), TestAugmentMCPWWWAuthenticate (overwrite semantics + scheme/host correctness + opt-out via enabled: false).

go vet clean across changed packages.

Architectural follow-ups (out of scope for this PR)

  • Static-client / non-DCR upstreams — for SaaS that don't allow public Dynamic Client Registration (most enterprise IdPs). Would extend protectedResourceMetadata with a staticClient block; Tyk synthesises a registration response from configured client_id/client_secret so mcp-remote stays oblivious.
  • AS metadata caching — the PRM doc is cached with TTL but AS metadata is currently re-fetched on every authorize/token round-trip. Easy win; ~40 LOC.
  • Service-account / token-broker modes — for use cases where the gateway should hold upstream credentials (client_credentials) or broker per-user tokens itself instead of being a transparent proxy. Different product surface.

Test plan

  • Unit tests pass (go test ./apidef/... ./gateway/... ./internal/mcp/...)
  • go vet clean
  • Live E2E: bare-minimum jira MCP API + Roo → browser opens, OAuth completes
  • Live E2E: bare-minimum notion MCP API + Roo → browser opens, OAuth completes (RFC 8707 path)
  • Existing PRM/PRM-suffix/JWT/AuthKey PRM tests still pass (no regression on static mode)

See also

Ticket Details

TT-17137
Status In Dev
Summary User should be easily able to set remote MCP as upstream

Generated at: 2026-05-07 14:55:39

andrei-tyk and others added 4 commits May 6, 2026 00:26
Two related fixes so an MCP API can transparently proxy an OAuth-protected
remote MCP server (e.g. Atlassian Rovo, Notion) without manual sidecars.

Routing fix: when the upstream URL has a path component (e.g. /v1/mcp),
OAuth/OIDC well-known discovery probes were incorrectly prefixed with
that path and 404'd against the upstream. The reverse-proxy director now
detects host-root discovery prefixes (RFC 9728, RFC 8414, OIDC) for MCP
APIs and routes them to the upstream host root.

PRM mirror mode: a new mode="mirror" on
x-tyk-api-gateway.server.authentication.protectedResourceMetadata makes
Tyk fetch the upstream's Protected Resource Metadata document, rewrite
the `resource` field to the gateway URL the client connected to (so
RFC 9728 §3.3 origin validation passes), cache with TTL, and serve at
the path-suffix URL clients actually probe
(`<gateway>/.well-known/oauth-protected-resource<listen-path>`, with
and without trailing slash). The mirror route is auto-registered for
any MCP API with PRM enabled. A response-side helper also augments
upstream 401 challenges with `resource_metadata=` when the upstream
sends a bare Bearer challenge so lax clients still find the doc.

Validated end-to-end against Atlassian Rovo with Roo / mcp-remote: a
single POST /tyk/mcps with `protectedResourceMetadata.mode: mirror` is
enough to put Tyk in front of the remote MCP and have the OAuth dance
complete in the browser. Notion works with the documented mcp-remote
`--resource` flag (Notion enforces RFC 8707 strictly; full transparent
support needs an OAuth-flow proxy, deferred to a follow-up).
Mirror-mode PRM rewrites the doc's `resource` field to the gateway URL
so RFC 9728 §3.3 origin validation passes, but strict authorization
servers (Notion as of 2026-Q2) then reject the client's authorize
request with `invalid_target` because the `resource` parameter the
client carries forward (per RFC 8707) is the gateway URL — not what
the upstream AS knows the resource by.

Mirror mode now interposes a per-API OAuth proxy so the two specs no
longer pull in opposite directions:

* The mirrored PRM doc redirects `authorization_servers` at a Tyk
  internal URL `<gateway>/__tyk-as/<api-id>` instead of the upstream
  AS, so clients discover Tyk's proxy first.

* Three new gateway-root routes per mirror-mode MCP API:
  - GET  /.well-known/oauth-authorization-server/__tyk-as/<api-id>
    serves the upstream's AS metadata with `authorization_endpoint`
    and `token_endpoint` rewritten to point at Tyk; `issuer` and
    `registration_endpoint` are preserved verbatim (DCR doesn't carry
    a `resource` parameter, so it can stay direct).
  - GET  /__tyk-as/<api-id>/authorize  302-redirects to the upstream
    authorize endpoint after swapping `resource=<gateway-url>` for
    `resource=<upstream-url>` in the query string.
  - POST /__tyk-as/<api-id>/token  forwards the token request to the
    upstream token endpoint with the same `resource` form-field swap
    and returns the upstream response unmodified.

Net effect: the client believes it's talking to one coherent OAuth
resource server at the gateway URL (origin check passes); the
upstream AS sees its own URL in the resource parameter (RFC 8707
check passes); Tyk reconciles the two on the wire. Validated end to
end against Atlassian Rovo and Notion via Roo with a single
POST /tyk/mcps per upstream and no client-side flags.

Tests cover the AS metadata rewrite, the authorize 302 with resource
rewrite, and the token forward with resource rewrite.
Make `mode: mirror` the implicit default for any MCP API that doesn't
configure protectedResourceMetadata explicitly. The minimal MCP API
definition is now just listen path + upstream — POSTing it to
/tyk/mcps is enough to put Tyk transparently in front of any
spec-compliant remote MCP, with no `authentication` block and no
`protectedResourceMetadata` block at all.

Implementation

* `EffectiveMode(isMCP bool)` resolves an empty `Mode` from the
  configuration shape: Resource set → static (back-compat for existing
  configs); Resource empty AND IsMCP → mirror; everything else →
  static. `Validate` and `IsMirrorMode` go through it.
* `OAS.ValidateForMCP(ctx, opts...)` runs the full validation chain
  with PRM checked in MCP context (Validate(true)). Used by the
  /tyk/mcps admin endpoint and by APISpec.Validate during reload, so
  on-disk MCP specs aren't rejected with "resource is required" at
  spec-load time after each reload.
* `GetPRMConfig` synthesises a default `{Enabled: true, Mode: ""}`
  for MCP APIs whose OAS doc has no explicit PRM block, so all the
  runtime plumbing (well-known suffix route, AS-proxy routes,
  WWW-Authenticate augmenter) engages without any opt-in config.

Bug fixes uncovered by the auto-mirror flow

* augmentMCPWWWAuthenticate previously read scheme/host from
  res.Request, which the reverse-proxy director has rewritten to the
  upstream URL. The injected `resource_metadata=` parameter pointed
  at the upstream rather than the gateway. Threaded the original
  inbound request through and use httputil.RequestScheme so the
  injected URL is the gateway's PRM endpoint. Call site moved into
  WrappedServeHTTP where the inbound request is in scope.
* augmentMCPWWWAuthenticate now overwrites an existing
  `resource_metadata=` instead of preserving it. Spec-compliant
  upstreams (Notion) already advertise their own URL via the
  parameter; preserving it pointed clients at the upstream's PRM
  doc, which then failed RFC 9728 §3.3 origin validation at the
  client and crashed the dance in registerClient on a fall-through
  to /register. Mirror mode now consistently redirects clients at
  Tyk's PRM endpoint regardless of what the upstream said.

Tests

Covers default-mirror-on-MCP, static back-compat (resource set keeps
static), explicit `enabled: false` opt-out, and the overwrite
semantics for upstream-supplied resource_metadata. All gateway,
apidef/oas, apidef/mcp, internal/mcp tests pass; go vet clean.

Verified end-to-end against Atlassian Rovo and Notion via Roo /
mcp-remote with the bare-minimum API definition (no auth block, no
PRM block).
@probelabs

probelabs Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

This PR enables Tyk to act as a zero-configuration, transparent reverse proxy for remote Multi-Channel Proxy (MCP) servers like Atlassian Rovo and Notion. It resolves three critical failure points in the OAuth flow, allowing MCP clients to authenticate through the gateway seamlessly.

  1. Routing Fix: Corrects an issue where OAuth/OIDC discovery probes (/.well-known/...) were incorrectly routed, causing 404 errors for upstreams hosted on non-root paths.
  2. Protected Resource Metadata (PRM) Mirror Mode: Introduces a new "mirror" mode, which is now the default for MCP APIs. Tyk fetches the upstream's PRM document, rewrites the resource URL to match the gateway's public address, and caches it. This satisfies client-side origin validation (RFC 9728).
  3. OAuth Proxy: Implements a new, per-API internal proxy that intercepts the OAuth flow. It rewrites the resource parameter in authorization and token requests from the gateway URL back to the upstream's URL, resolving invalid_target errors from strict authorization servers (RFC 8707).
  4. Automatic Engagement: The entire mechanism is enabled by default for any API marked with applicationProtocol: "mcp", requiring no specific PRM or authentication configuration from the user.

Files Changed Analysis

The changes are well-structured across the API definition, core gateway logic, and a new internal package:

  • apidef/: The OAS schema and validation logic are updated to support the new PRM mirror mode. resource is no longer required for MCP APIs, and a new ValidateForMCP function correctly handles the new default behavior.
  • gateway/: This contains the core implementation.
    • reverse_proxy.go: Implements the routing fix for discovery probes and adds logic to augment the WWW-Authenticate header on 401 responses.
    • mw_protected_resource.go: Contains the logic for PRM mirror mode, including fetching, rewriting, and serving the upstream's PRM document.
    • mcp_oauth_proxy.go (new): A new file that houses the OAuth proxy, handling authorization redirects and token request forwarding with resource parameter rewriting.
    • api_loader.go: Registers the new routes required for the PRM suffix path and the per-API OAuth proxy endpoints.
    • model_apispec.go: The GetPRMConfig method is updated to synthesize a default PRM configuration for MCP APIs, automatically activating mirror mode.
  • internal/mcp/ (new): A new package for shared MCP-specific logic.
    • discovery.go: Identifies discovery endpoints that require special routing.
    • prm_cache.go: Implements a new TTL cache for upstream PRM documents.
  • docs/: A new Jupyter notebook (docs/mcp-remote-proxies.ipynb) is added as an operator's guide for testing and demonstration.
  • Testing: Comprehensive unit and integration tests are added across all new and modified packages to validate the routing, PRM mirroring, and OAuth proxy flows.

Architecture & Impact Assessment

  • What this PR accomplishes: It significantly enhances Tyk's capabilities, transforming it from a simple transparent proxy into an application-protocol-aware gateway that can intelligently mediate complex, OAuth-protected services with zero user configuration.
  • Key technical changes introduced:
    1. Protocol-Aware Routing: The reverse proxy director now contains conditional logic based on the API's protocol (IsMCP()) and request path to alter its routing behavior for discovery endpoints.
    2. Dynamic Metadata Generation: Tyk now fetches, modifies, and serves security metadata (PRM) from an upstream source, rather than relying solely on static configuration.
    3. OAuth Flow Interception: A new internal, per-API proxy endpoint (/__tyk-as/<api-id>) is introduced to mediate the multi-step OAuth flow, which occurs outside the primary API listen path.
    4. Response Header Rewriting: Tyk modifies the WWW-Authenticate header on upstream 401 responses to guide clients through the correct, gateway-mediated discovery process.
  • Affected system components: The changes primarily impact the Gateway Reverse Proxy, Middleware (PRM), and the API Loader. A new internal mcp package and a PRMCache have been added.

OAuth Flow Visualization

sequenceDiagram
    participant Client
    participant Tyk Gateway
    participant Upstream MCP
    participant "Upstream Auth Server (AS)"

    Client->>+Tyk Gateway: 1. POST / (MCP request)
    Tyk Gateway->>+Upstream MCP: Forward request
    Upstream MCP-->>-Tyk Gateway: 2. 401 Unauthorized + WWW-Authenticate
    Tyk Gateway->>Tyk Gateway: 3. Augment WWW-Authenticate header with<br>resource_metadata=<gateway-prm-url>
    Tyk Gateway-->>-Client: 401 with augmented header

    Client->>+Tyk Gateway: 4. GET /.well-known/oauth-protected-resource/... (PRM probe)
    Tyk Gateway->>Tyk Gateway: 5. (Mirror Mode) Fetch upstream PRM, rewrite `resource` & `authorization_servers`
    Tyk Gateway-->>-Client: 6. Mirrored PRM Document (points to Tyk AS Proxy)

    Client->>+Tyk Gateway: 7. GET /__tyk-as/<api-id>/authorize?resource=<gateway-url>
    Tyk Gateway->>Tyk Gateway: 8. Rewrite `resource` param to <upstream-url>
    Tyk Gateway-->>-"Upstream Auth Server (AS)": 9. 302 Redirect to AS with rewritten param
    Note over "Upstream Auth Server (AS)", Client: (User completes OAuth dance via browser)

    Client->>+Tyk Gateway: 10. POST /__tyk-as/<api-id>/token
    Tyk Gateway->>Tyk Gateway: 11. Rewrite `resource` param to <upstream-url>
    Tyk Gateway->>+"Upstream Auth Server (AS)": 12. Forward token request
    "Upstream Auth Server (AS)"-->>-Tyk Gateway: 13. Access Token
    Tyk Gateway-->>-Client: 14. Access Token
Loading

Scope Discovery & Context Expansion

This PR establishes a powerful new pattern of protocol-aware proxying within Tyk. While scoped to MCP, the architectural components are generalizable:

  • The conditional, path-based routing logic in gateway/reverse_proxy.go could be extended to support other protocols with out-of-band discovery mechanisms.
  • The per-API internal proxy endpoint (/__tyk-as/...) introduced in gateway/mcp_oauth_proxy.go and registered in gateway/api_loader.go provides a robust mechanism for handling complex, multi-step interactions tied to a specific API but living outside its primary listen path.
  • The use of applicationProtocol: "mcp" as an explicit opt-in provides a clear and extensible pattern for activating specialized handling for different protocols.
  • This change deepens Tyk's capabilities, moving it towards a more sophisticated service proxy that can adapt to the application-level protocols of its upstreams.
Metadata
  • Review Effort: 5 / 5
  • Primary Label: feature

Powered by Visor from Probelabs

Last updated: 2026-05-07T14:58:24.362Z | Triggered by: pr_updated | Commit: ce09452

💡 TIP: You can chat with Visor using /visor ask <your question>

@probelabs

probelabs Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Security Issues (2)

Severity Location Issue
🟡 Warning gateway/mcp_oauth_proxy.go:285
The HTTP client call in `fetchUpstreamASMetadata` to fetch upstream Authorization Server (AS) metadata does not have an explicit timeout. The function uses `http.DefaultClient` with the incoming request's context, which may not have a timeout configured. If the upstream AS metadata endpoint is slow or unresponsive, it could lead to resource exhaustion by holding connections and goroutines for an extended period.
💡 SuggestionApply a short, explicit timeout to the context used for fetching the upstream AS metadata to prevent long-running requests from consuming gateway resources. A `context.WithTimeout` should be used before making the `http.DefaultClient.Do(req)` call, similar to the pattern used in `gateway/mw_protected_resource.go` for fetching PRM documents.
🟡 Warning docs/mcp-remote-proxies.ipynb:41
A hardcoded default gateway secret is present in the Jupyter notebook used for documentation and testing. While this is the default secret, committing credentials, even default ones, into source control is a security risk. It can be inadvertently copied to production environments or encourage insecure practices.
💡 SuggestionRemove the hardcoded secret from the notebook. Replace it with a placeholder and add instructions for the user to set it via an environment variable or another secure mechanism before running the notebook.
🔧 Suggested Fix
import os

...

Read from environment variable, or use a placeholder

GATEWAY_SECRET = os.environ.get("TYK_GW_SECRET", "<YOUR_GATEWAY_SECRET>")

Architecture Issues (2)

Severity Location Issue
🟡 Warning gateway/mcp_oauth_proxy.go:185
The OAuth proxy uses `http.DefaultClient` for outbound requests to the upstream authorization server. This client has no timeouts and bypasses the gateway's configurable HTTP transport settings, which are used by the main reverse proxy for robustness and consistency. Using the default client can lead to connections hanging indefinitely and ignores custom transport configurations (e.g., TLS settings, proxy settings) defined in the gateway's configuration.
💡 SuggestionInstantiate and use a dedicated `http.Client` that is configured with a transport similar to the one used by the main gateway reverse proxy. This ensures that outbound connections from this new component adhere to the gateway's overall network policies and timeout configurations. The gateway likely has a central factory or helper for creating such clients that should be reused here.
🟡 Warning gateway/api_loader.go:961-970
The automatic registration of PRM suffix routes and the OAuth proxy for MCP APIs is a significant, implicitly-enabled behavior. The log messages for this are set to `debug` level (`mainLog...Debugf(...)`). This makes it difficult for operators to understand why these gateway-root-level routes have appeared and which API they belong to without enabling verbose logging. Given that this is a powerful convention-over-configuration feature, its activation should be more visible in standard operational logs.
💡 SuggestionChange the log level for the messages at lines 961 and 970 from `Debugf` to `Infof`. This will ensure that whenever an MCP API is loaded and these routes are automatically registered, a clear message is printed in the standard gateway logs, improving transparency and diagnosability for operators.

Performance Issues (2)

Severity Location Issue
🟠 Error gateway/mcp_oauth_proxy.go:73
Authorization Server (AS) metadata is fetched from the upstream on every request to the OAuth proxy endpoints (`/authorize`, `/token`). The `resolveUpstreamASMetadata` function, called by all proxy handlers, triggers `fetchUpstreamASMetadata`, which performs network requests without any caching. This adds significant latency to every step of the user authentication and token retrieval flow.
💡 SuggestionImplement a cache for Authorization Server metadata. The fetched metadata should be stored with a reasonable TTL (e.g., 5 minutes), similar to the pattern used for the PRM cache (`internal/mcp/prm_cache.go`). This will reduce the number of outbound requests and significantly improve the performance of the OAuth flow.
🟡 Warning gateway/reverse_proxy.go:498
The regular expression in `replaceOrAppendResourceMetadata` is compiled on every function call. This function is part of the response handling path for 401 Unauthorized responses from MCP upstreams. Compiling a regex repeatedly is computationally expensive and introduces unnecessary overhead on a request-handling path.
💡 SuggestionCompile the regular expression once at the package level and reuse the compiled object. This can be done by declaring a package-level variable, for example: `var rxResourceMetadata = regexp.MustCompile("(?i)resource_metadata=\"[^\"]*\"")`.

Quality Issues (1)

Severity Location Issue
🟡 Warning gateway/reverse_proxy.go:498
The regular expression `rxResourceMetadata` is compiled on every call to `replaceOrAppendResourceMetadata`. For performance, regular expressions should be compiled only once at the package level and reused.
💡 SuggestionMove the `regexp.MustCompile` call to a package-level variable declaration to ensure the regular expression is compiled only once when the package is initialized.
🔧 Suggested Fix
var rxResourceMetadata = regexp.MustCompile(`(?i)resource_metadata="[^"]*"`)

func replaceOrAppendResourceMetadata(headerValue, prmURL string) string {
replacement := fmt.Sprintf(resource_metadata=&#34;%s&#34;, prmURL)
if rxResourceMetadata.MatchString(headerValue) {
return rxResourceMetadata.ReplaceAllString(headerValue, replacement)
}
return headerValue + ", " + replacement
}


Powered by Visor from Probelabs

Last updated: 2026-05-07T14:57:38.909Z | Triggered by: pr_updated | Commit: ce09452

💡 TIP: You can chat with Visor using /visor ask <your question>

andrei-tyk added 4 commits May 7, 2026 14:23
A previous change inferred mirror mode from `Resource` being empty for
MCP APIs. That regressed two existing tests in TestValidateMCP_PRM
that POST a PRM block with only `authorizationServers` set (no
resource): the user is clearly midway through configuring static mode
and expects "resource is required" feedback, not a silent default to
mirror.

Tighten EffectiveMode so any static-field hint — `Resource` or
`AuthorizationServers` populated — keeps the empty-mode path on
static. Mirror is the implicit default only when neither field is set
and the API is MCP, which is the bare-minimum-config flow this PR is
aiming for.

Adds a regression case to TestProtectedResourceMetadata_IsMirrorMode.
- Extract `resolveUpstreamASMetadata` from the three handlers that all
  did the same firstAuthorizationServer + fetchUpstreamASMetadata
  two-step with identical bad-gateway error paths. The handlers now
  early-exit with a single `if !ok { return }`, dedupes the literal
  error strings (caught by SonarQube as 3x "upstream authorization
  server unavailable" / "upstream AS metadata unavailable"), and
  drops the cognitive complexity of authorizeProxyHandler (17 -> 9)
  and tokenProxyHandler (19 -> 10).
- Move the per-handler error literals to package constants
  (errUpstreamASUnavailable, errUpstreamASMetadataUnavail,
  errUpstreamMissingAuthorizeEP, errUpstreamMissingTokenEP,
  errInvalidUpstreamAuthorizeURL).
- Pull the authorize-URL query merge out of the handler into
  `mergedAuthorizeQuery`.
- Pull the token-endpoint POST construction out into
  `buildTokenRequest` and the upstream response mirror into
  `writeUpstreamResponse`.

While refactoring caught a real bug: the previous code did
context.WithTimeout inside the helper that built the token request
and stashed cancel without anyone deferring it; cancel fired the
moment the helper returned, killing the in-flight upstream request.
Lifted the timeout context into the handler with `defer cancel()`
so it covers the full Do + body-read.

Behavior unchanged otherwise. Verified live against Atlassian Rovo
via mcp-remote: discovery, DCR, browser-opened authorize URL with
the resource parameter rewritten as before.
Pushes new-code coverage past 80%. Tests cover:

gateway/mcp_oauth_proxy_test.go (new):
* AS-proxy metadata endpoint serves a rewritten doc (issuer
  preserved, authorize/token endpoints repointed at Tyk).
* AS-proxy authorize handler 302s with the resource parameter
  rewritten gateway-URL -> upstream-URL.
* AS-proxy token handler forwards the POST and the upstream sees
  resource rewritten on the form body.
* Both authorize and token handlers return 502 when upstream PRM /
  AS metadata is missing or unreachable.
* fetchUpstreamASMetadata path-suffix, path-prefix and host-root
  variants, all-404 fallback, malformed URL rejection.
* registerMCPPRMSuffixRoutes early-return guards (nil spec, non-MCP,
  PRM disabled, root listen path).
* replaceOrAppendResourceMetadata: append-if-absent, replace-if-
  present, case-insensitive match.
* PRM mirror error path: serveMirroredPRM returns 502 when upstream
  is unreachable.
* rewriteResourceParam unit cases.

internal/mcp/prm_cache_test.go (extended):
* FetchUpstreamPRM error variants (empty URL, malformed JSON, 404).
* PRMCache.Invalidate.
* PRMDocument nil-safety (Resource, SetResource, MarshalJSON).
* SetResource initialises Raw lazily.
* NewPRMCache(0) defaults to DefaultPRMCacheTTL.

apidef/oas/validate_for_mcp_test.go (new):
* OAS.ValidateForMCP passes empty PRM (mirror by default for MCP);
  plain Validate rejects same config; explicit static configs pass
  either path; static-MCP without authorizationServers still
  rejected; disabled or absent PRM block passes both.
* EffectiveMode resolution table — explicit overrides win, shape-
  driven defaults match.

All green; go vet clean.
Mirror is the implicit default for MCP APIs without static PRM fields,
and shape-based inference (resource/authorizationServers populated
=> static, otherwise mirror) covers every sane case. The explicit
`mode` field and `upstreamPRMUrl` override never carried real product
value: setting `mode: "mirror"` on a config with `resource` populated
would just override what the operator obviously meant; setting
`upstreamPRMUrl` is unnecessary because the path-suffix derivation
from the upstream URL works for every spec-compliant remote MCP.

Removed:
* `Mode` and `UpstreamPRMUrl` fields on
  `apidef/oas.ProtectedResourceMetadata`.
* `PRMModeStatic` / `PRMModeMirror` constants.
* `EffectiveMode(isMCP bool) string` — superseded by the simpler
  `IsMirrorMode(isMCP bool) bool`, which now contains the entire mode
  resolution logic.
* JSON-schema entries for `mode` and `upstreamPRMUrl` in both
  `apidef/oas/schema/x-tyk-api-gateway*.json` and
  `apidef/mcp/schema/x-tyk-api-gateway.json`.

Validation logic preserved: static mode requires `resource`, plus
`authorizationServers` for MCP APIs. Mirror skips both.
`mw_protected_resource.go::upstreamPRMDoc` always derives the PRM
URL via `mcp.DeriveUpstreamPRMURL` now.

Tests pruned to match: `EffectiveMode` cases gone, mirror-mode test
fixtures no longer set `Mode` (the empty struct + IsMCP context is
the canonical mirror-mode signal). Live E2E against Atlassian Rovo
via mcp-remote unchanged: bare-minimum API def => mirror engages,
AS-proxy 302s with resource parameter rewritten upstream URL.
@andrei-tyk
andrei-tyk force-pushed the TT-17137-user-should-be-easily-able-to-set-remote-mcp-as-upstream branch from 10856c0 to 1078126 Compare May 7, 2026 13:08
Formatting: gofmt-fix apidef/oas/validate_for_mcp_test.go and
gateway/mw_protected_resource_test.go.

Production unchecked errors:
* gateway/api_loader.go: log a warning when json.Encoder.Encode
  fails on the PRM suffix-route handler instead of dropping the
  error silently.
* gateway/mcp_oauth_proxy.go: same for the AS-proxy metadata
  encoder; log when io.Copy on the upstream-response mirror fails;
  return a wrapped error when io.ReadAll on AS metadata fails (was
  swallowed, masking decode-time errors).
* internal/mcp/prm_cache.go: replace the type-assertion `_, _`
  pattern in PRMDocument.Resource with the if-ok form; rename the
  shadowing `url` parameter on SetResource to resourceURL; surface
  io.ReadAll failures on PRM error-body decode instead of
  swallowing them.

Production lint:
* gateway/mw_protected_resource.go: rename unused parameter prm to
  _ on serveMirroredPRM (the doc is fetched via upstreamPRMDoc
  inside).
* gateway/reverse_proxy.go: rename the shadowing `header` parameter
  on replaceOrAppendResourceMetadata to headerValue.

Test-side annotations:
* Added //nolint:errcheck on httptest fixtures where w.Write,
  fmt.Fprintf, and r.ParseForm legitimately discard their return
  values (server-side test helpers can't surface failures
  meaningfully).
* Replaced http.NewRequest discards with `errReq := …;
  require.NoError(t, errReq)` in test bodies.
* Replaced url.Parse discards in reverse_proxy_test.go with proper
  error checks.
* Renamed unused closure parameters (req in CheckRedirect,
  t in subtests) to _.
* Converted `switch { case r.URL.Path == … }` blocks to tagged
  `switch r.URL.Path` per QF1002; collapsed a single-case switch
  to an if.

go vet clean. All MCP-mirror tests pass. Live E2E against
Atlassian Rovo via mcp-remote unchanged: bare-minimum API def
=> mirror engages, PRM doc rewritten, AS-proxy 302s with
resource parameter rewritten to upstream URL, DCR completes,
browser opens. No behavior change.
* mcp_oauth_proxy.go: replace `_, _ := metadata[...].(string)` and
  `_, _ := doc.Raw[...].([]any)` patterns with the if-ok form. Three
  call sites (authorize handler, token handler,
  firstAuthorizationServer); each was a type assertion paired with
  an emptiness check, where a missing key returns the zero value
  and is then caught — but errcheck (and SonarQube) flag the
  discarded ok value. The if-ok form is clearer and surfaces
  malformed upstream metadata as an explicit error from
  firstAuthorizationServer.

* mw_protected_resource_test.go: move the //nolint:errcheck
  directive for the multi-line io.WriteString call from the
  closing line to the line above the call. nolint comments only
  apply to the line they're on, so the trailing-comment placement
  wasn't suppressing the warning on the call's first line.

go vet clean. Live E2E unchanged: AS-proxy 302s with resource
parameter rewritten to upstream URL; AS metadata served with
authorize/token endpoints repointed at the gateway. PRM/auth flow
verified end to end.
@sonarqubecloud

sonarqubecloud Bot commented May 7, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
1 New issue
0 Accepted issues

Measures
0 Security Hotspots
83.4% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@lghiur
lghiur merged commit 4e3b745 into master May 7, 2026
52 of 55 checks passed
@lghiur
lghiur deleted the TT-17137-user-should-be-easily-able-to-set-remote-mcp-as-upstream branch May 7, 2026 18:35
@lghiur

lghiur commented May 7, 2026

Copy link
Copy Markdown
Collaborator

/release to release-5.13

@lghiur

lghiur commented May 7, 2026

Copy link
Copy Markdown
Collaborator

/release to release-5.13.0

@probelabs

probelabs Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

✅ Cherry-pick successful. A PR was created: #8182

@probelabs

probelabs Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

✅ Cherry-pick successful. A PR was created: #8183

lghiur pushed a commit that referenced this pull request May 7, 2026
… routing fix + auto-mirror PRM + OAuth proxy (#8181) (#8182)

[TT-17137] Support remote MCPs as upstream — routing fix + auto-mirror
PRM + OAuth proxy (#8181)

## Summary

Make Tyk able to put a transparent reverse proxy in front of any
spec-compliant remote MCP server (Atlassian Rovo, Notion, etc.) so MCP
clients (Roo, Claude Desktop, anything that uses `mcp-remote`) can
complete the OAuth dance through the gateway with zero per-upstream
knobs. Three commits, each fixing a distinct failure point along the
wire.

The minimal MCP API definition is now just listen path + upstream:

```json
{
  "openapi": "3.0.3",
  "info": {"title": "jira", "version": "1.0.0"},
  "paths": {"/": {"get": {...}, "post": {...}}},
  "x-tyk-api-gateway": {
    "info": {"id": "jira-mcp-remote", "name": "jira", "state": {"active": true}},
    "server": {"listenPath": {"value": "/jira/", "strip": true}},
    "upstream": {"url": "https://mcp.atlassian.com/v1/mcp/authv2"}
  }
}
```

POST that to `/tyk/mcps`. Mirror mode auto-engages — no `authentication`
block, no `protectedResourceMetadata` block. The full OAuth dance
completes end-to-end through Tyk.

## What was broken (three failure modes, in order)

1. **Routing**: when the upstream URL had a path component (e.g.
`/v1/mcp/authv2`), `.well-known/*` discovery probes were prefixed with
that path and 404'd — well-known endpoints live at the host root.
2. **RFC 9728 §3.3 origin mismatch**: even with routing fixed,
transparent-proxying the upstream's PRM doc gives a `resource` field
that doesn't match the URL the client connected to — strict clients
(mcp-remote, Roo) refuse to authenticate.
3. **RFC 8707 `invalid_target`**: even with PRM rewritten, the client
sends the gateway URL as the `resource` parameter to the upstream's
authorize endpoint. Strict authorization servers (Notion) only accept
the upstream URL and reject with `invalid_target`.

## What we built

* **TT-17137 routing fix** (`gateway/reverse_proxy.go`): MCP-aware
host-root branch in the reverse-proxy director routes well-known
discovery probes (RFC 9728 / RFC 8414 / OIDC) to the upstream host root,
ignoring the upstream URL's path.
* **PRM mirror mode** (`apidef/oas/authentication.go`,
`gateway/mw_protected_resource.go`, `gateway/api_loader.go`): new `mode:
"mirror"` fetches the upstream's PRM doc, rewrites `resource` to the
gateway URL, caches with TTL, auto-serves at the path-suffix URL clients
actually probe
(`<gateway>/.well-known/oauth-protected-resource<listen-path>`).
* **OAuth-flow proxy** (`gateway/mcp_oauth_proxy.go`): mirror mode
redirects `authorization_servers` at a per-API Tyk URL. Tyk serves a
synthesised AS metadata document with
`authorization_endpoint`/`token_endpoint` pointing at itself, then
302-redirects the authorize request and forwards token POSTs after
rewriting the `resource` parameter from gateway URL to upstream URL.
Reconciles RFC 9728 §3.3 vs. RFC 8707 on the wire.
* **Auto-mirror default**: `EffectiveMode(isMCP)` resolves empty mode by
configuration shape — Resource set → static (back-compat); Resource
empty + MCP → mirror; everything else → static. `OAS.ValidateForMCP`
threads MCP context through OAS-level validation so empty configs don't
get rejected with "resource is required". `GetPRMConfig` synthesises a
default for MCP APIs without an explicit PRM block, so the runtime
plumbing engages with zero opt-in.
* **WWW-Authenticate rewriter** (`augmentMCPWWWAuthenticate`): replaces
(not appends) the `resource_metadata=` parameter on upstream 401s so
clients are pointed at the gateway's PRM endpoint. Uses the inbound
request's scheme/host (not the rewritten outbound).

## Validated end-to-end

* **Atlassian Rovo** (`https://mcp.atlassian.com/v1/mcp/authv2`) — Roo
browser flow completes, tools list returned.
* **Notion** (`https://mcp.notion.com/mcp`) — same. RFC 8707
`invalid_target` resolved by the AS proxy's `resource` parameter
rewrite.

## Tests

* Unit: `apidef/oas/authentication_test.go` (`Validate`,
`EffectiveMode`, `IsMirrorMode`), `internal/mcp/discovery_test.go`
(host-root prefix matching), `internal/mcp/prm_cache_test.go` (TTL cache
+ URL derivation + upstream PRM fetch).
* Integration:
`gateway/reverse_proxy_test.go::TestReverseProxyMCPHostRootDiscovery`
(routing fix; non-MCP regression guard),
`gateway/mw_protected_resource_test.go::TestPRMMirrorMode_SuffixRoute`
(PRM serving + rewrite), `TestPRMMirrorMode_OAuthProxy` (AS metadata
rewrite, authorize 302, token forward), `TestAugmentMCPWWWAuthenticate`
(overwrite semantics + scheme/host correctness + opt-out via `enabled:
false`).

`go vet` clean across changed packages.

## Architectural follow-ups (out of scope for this PR)

* **Static-client / non-DCR upstreams** — for SaaS that don't allow
public Dynamic Client Registration (most enterprise IdPs). Would extend
`protectedResourceMetadata` with a `staticClient` block; Tyk synthesises
a registration response from configured `client_id`/`client_secret` so
mcp-remote stays oblivious.
* **AS metadata caching** — the PRM doc is cached with TTL but AS
metadata is currently re-fetched on every authorize/token round-trip.
Easy win; ~40 LOC.
* **Service-account / token-broker modes** — for use cases where the
gateway should hold upstream credentials (`client_credentials`) or
broker per-user tokens itself instead of being a transparent proxy.
Different product surface.

## Test plan

- [x] Unit tests pass (`go test ./apidef/... ./gateway/...
./internal/mcp/...`)
- [x] `go vet` clean
- [x] Live E2E: bare-minimum jira MCP API + Roo → browser opens, OAuth
completes
- [x] Live E2E: bare-minimum notion MCP API + Roo → browser opens, OAuth
completes (RFC 8707 path)
- [x] Existing PRM/PRM-suffix/JWT/AuthKey PRM tests still pass (no
regression on static mode)

## See also

- [`docs/mcp-remote-oauth-flow.md`](docs/mcp-remote-oauth-flow.md) —
full wire diagram of the OAuth flow with the three failure-mode
diagrams.
- [`docs/mcp-remote-proxies.ipynb`](docs/mcp-remote-proxies.ipynb) —
operator notebook for both Jira and Notion proxies.





<!---TykTechnologies/jira-linter starts here-->

### Ticket Details

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17137" title="TT-17137"
target="_blank">TT-17137</a>
</summary>

|         |    |
|---------|----|
| Status  | Merge |
| Summary | User should be easily able to set remote MCP as upstream |

Generated at: 2026-05-07 18:36:33

</details>

<!---TykTechnologies/jira-linter ends here-->


[TT-17137]:
https://tyktech.atlassian.net/browse/TT-17137?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Co-authored-by: andrei-tyk <[email protected]>
lghiur pushed a commit that referenced this pull request May 7, 2026
… — routing fix + auto-mirror PRM + OAuth proxy (#8181) (#8183)

[TT-17137] Support remote MCPs as upstream — routing fix + auto-mirror
PRM + OAuth proxy (#8181)

## Summary

Make Tyk able to put a transparent reverse proxy in front of any
spec-compliant remote MCP server (Atlassian Rovo, Notion, etc.) so MCP
clients (Roo, Claude Desktop, anything that uses `mcp-remote`) can
complete the OAuth dance through the gateway with zero per-upstream
knobs. Three commits, each fixing a distinct failure point along the
wire.

The minimal MCP API definition is now just listen path + upstream:

```json
{
  "openapi": "3.0.3",
  "info": {"title": "jira", "version": "1.0.0"},
  "paths": {"/": {"get": {...}, "post": {...}}},
  "x-tyk-api-gateway": {
    "info": {"id": "jira-mcp-remote", "name": "jira", "state": {"active": true}},
    "server": {"listenPath": {"value": "/jira/", "strip": true}},
    "upstream": {"url": "https://mcp.atlassian.com/v1/mcp/authv2"}
  }
}
```

POST that to `/tyk/mcps`. Mirror mode auto-engages — no `authentication`
block, no `protectedResourceMetadata` block. The full OAuth dance
completes end-to-end through Tyk.

## What was broken (three failure modes, in order)

1. **Routing**: when the upstream URL had a path component (e.g.
`/v1/mcp/authv2`), `.well-known/*` discovery probes were prefixed with
that path and 404'd — well-known endpoints live at the host root.
2. **RFC 9728 §3.3 origin mismatch**: even with routing fixed,
transparent-proxying the upstream's PRM doc gives a `resource` field
that doesn't match the URL the client connected to — strict clients
(mcp-remote, Roo) refuse to authenticate.
3. **RFC 8707 `invalid_target`**: even with PRM rewritten, the client
sends the gateway URL as the `resource` parameter to the upstream's
authorize endpoint. Strict authorization servers (Notion) only accept
the upstream URL and reject with `invalid_target`.

## What we built

* **TT-17137 routing fix** (`gateway/reverse_proxy.go`): MCP-aware
host-root branch in the reverse-proxy director routes well-known
discovery probes (RFC 9728 / RFC 8414 / OIDC) to the upstream host root,
ignoring the upstream URL's path.
* **PRM mirror mode** (`apidef/oas/authentication.go`,
`gateway/mw_protected_resource.go`, `gateway/api_loader.go`): new `mode:
"mirror"` fetches the upstream's PRM doc, rewrites `resource` to the
gateway URL, caches with TTL, auto-serves at the path-suffix URL clients
actually probe
(`<gateway>/.well-known/oauth-protected-resource<listen-path>`).
* **OAuth-flow proxy** (`gateway/mcp_oauth_proxy.go`): mirror mode
redirects `authorization_servers` at a per-API Tyk URL. Tyk serves a
synthesised AS metadata document with
`authorization_endpoint`/`token_endpoint` pointing at itself, then
302-redirects the authorize request and forwards token POSTs after
rewriting the `resource` parameter from gateway URL to upstream URL.
Reconciles RFC 9728 §3.3 vs. RFC 8707 on the wire.
* **Auto-mirror default**: `EffectiveMode(isMCP)` resolves empty mode by
configuration shape — Resource set → static (back-compat); Resource
empty + MCP → mirror; everything else → static. `OAS.ValidateForMCP`
threads MCP context through OAS-level validation so empty configs don't
get rejected with "resource is required". `GetPRMConfig` synthesises a
default for MCP APIs without an explicit PRM block, so the runtime
plumbing engages with zero opt-in.
* **WWW-Authenticate rewriter** (`augmentMCPWWWAuthenticate`): replaces
(not appends) the `resource_metadata=` parameter on upstream 401s so
clients are pointed at the gateway's PRM endpoint. Uses the inbound
request's scheme/host (not the rewritten outbound).

## Validated end-to-end

* **Atlassian Rovo** (`https://mcp.atlassian.com/v1/mcp/authv2`) — Roo
browser flow completes, tools list returned.
* **Notion** (`https://mcp.notion.com/mcp`) — same. RFC 8707
`invalid_target` resolved by the AS proxy's `resource` parameter
rewrite.

## Tests

* Unit: `apidef/oas/authentication_test.go` (`Validate`,
`EffectiveMode`, `IsMirrorMode`), `internal/mcp/discovery_test.go`
(host-root prefix matching), `internal/mcp/prm_cache_test.go` (TTL cache
+ URL derivation + upstream PRM fetch).
* Integration:
`gateway/reverse_proxy_test.go::TestReverseProxyMCPHostRootDiscovery`
(routing fix; non-MCP regression guard),
`gateway/mw_protected_resource_test.go::TestPRMMirrorMode_SuffixRoute`
(PRM serving + rewrite), `TestPRMMirrorMode_OAuthProxy` (AS metadata
rewrite, authorize 302, token forward), `TestAugmentMCPWWWAuthenticate`
(overwrite semantics + scheme/host correctness + opt-out via `enabled:
false`).

`go vet` clean across changed packages.

## Architectural follow-ups (out of scope for this PR)

* **Static-client / non-DCR upstreams** — for SaaS that don't allow
public Dynamic Client Registration (most enterprise IdPs). Would extend
`protectedResourceMetadata` with a `staticClient` block; Tyk synthesises
a registration response from configured `client_id`/`client_secret` so
mcp-remote stays oblivious.
* **AS metadata caching** — the PRM doc is cached with TTL but AS
metadata is currently re-fetched on every authorize/token round-trip.
Easy win; ~40 LOC.
* **Service-account / token-broker modes** — for use cases where the
gateway should hold upstream credentials (`client_credentials`) or
broker per-user tokens itself instead of being a transparent proxy.
Different product surface.

## Test plan

- [x] Unit tests pass (`go test ./apidef/... ./gateway/...
./internal/mcp/...`)
- [x] `go vet` clean
- [x] Live E2E: bare-minimum jira MCP API + Roo → browser opens, OAuth
completes
- [x] Live E2E: bare-minimum notion MCP API + Roo → browser opens, OAuth
completes (RFC 8707 path)
- [x] Existing PRM/PRM-suffix/JWT/AuthKey PRM tests still pass (no
regression on static mode)

## See also

- [`docs/mcp-remote-oauth-flow.md`](docs/mcp-remote-oauth-flow.md) —
full wire diagram of the OAuth flow with the three failure-mode
diagrams.
- [`docs/mcp-remote-proxies.ipynb`](docs/mcp-remote-proxies.ipynb) —
operator notebook for both Jira and Notion proxies.





<!---TykTechnologies/jira-linter starts here-->

### Ticket Details

<details>
<summary>
<a href="https://tyktech.atlassian.net/browse/TT-17137" title="TT-17137"
target="_blank">TT-17137</a>
</summary>

|         |    |
|---------|----|
| Status  | Merge |
| Summary | User should be easily able to set remote MCP as upstream |

Generated at: 2026-05-07 18:36:49

</details>

<!---TykTechnologies/jira-linter ends here-->


[TT-17137]:
https://tyktech.atlassian.net/browse/TT-17137?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Co-authored-by: andrei-tyk <[email protected]>
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.

3 participants