Skip to content

release: MCP DCR + consent-proxy clientid fixes → PROD (#2709 #2710 #2711) - #2712

Merged
jung-thomas merged 6 commits into
mainfrom
DEV
Oct 11, 2026
Merged

jung-thomas merged 6 commits into
mainfrom
DEV

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Release: MCP DCR + consent-proxy clientid fixes → PROD

Promotes the 3 MCP consent-proxy changes verified end-to-end on DEV. Scope is entirely MCP OAuth; no other subsystem touched.

Commits

Why

Native-HTTP OAuth MCP clients (Claude Code/Desktop, Joule Work Desktop) bootstrap via DCR. Before this, the consent proxy had no /register, so the advertised "native HTTP, no bridge" path failed — the only working path was the mcp-remote bridge with an explicit, env-specific client_id. DCR now hands clients the correct per-env clientid automatically, eliminating the wrong-env aud-mismatch failure class (the multi-day PROD saga) by construction.

DEV verification (2026-10-11)

  • POST /mcp-oauth/register → 201, client_id = 0b1e8b56… (srv-mcp's, not the approuter's cf2ea5eb…), no secret.
  • Full native-HTTP OAuth + DCR end-to-end via mcp-remote (no --static-oauth-client-info): discovery → DCR → consent → IAS login → token → Connected, no WrongAudience 401.
  • 63 unit tests green; smoke-verified post-deploy.

PROD config

deploy/prod.mtaext pins MCP_RESOURCE_CLIENT_ID: 5a7cb5fd-a0f0-4678-8f51-f8c471e0ab34 (PROD srv-mcp's clientid). The PROD IAS client 5a7cb5fd… already has https://developers.sap.com/mcp-oauth/callback registered (done during the earlier saga). No new secrets; MCP_CONSENT_STATE_SECRET already seeded in PROD credstore.

🤖 Generated with Claude Code

…ients

Reverses the incorrect unification from #2700. DEV and PROD each have their OWN
tutorials-identity service instance with a DISTINCT clientid:
  - DEV  = 0b1e8b56-5f5d-4ebf-a9e9-28aae2964236
  - PROD = 5a7cb5fd-a0f0-4678-8f51-f8c471e0ab34

#2700 wrongly claimed a single shared client (0b1e8b56…) for both envs. That is
why PROD failed live with "server rejected a token it had just issued": the
interactive id_token's aud = the supplied client_id (0b1e8b56…), but PROD's
tutorials-srv-mcp validates aud against its OWN instance clientid (5a7cb5fd…) →
WrongAudience 401 after a successful browser login. Proven on PROD 2026-10-11:
an M2M JWT (aud=5a7cb5fd…) returns 200 on /mcp-auth/api, and the interactive
flow with client_id=5a7cb5fd… now connects end-to-end.

Also fixed in IAS console (config, not code): added
https://developers.sap.com/mcp-oauth/callback to the PROD client (5a7cb5fd…,
app 8164289a…) redirect URIs — it had only the loopback callbacks. (The earlier
#2667-era addition had gone onto the DEV client by mistake.)

Updates: env table (Production→5a7cb5fd…, Dev→0b1e8b56…), both mcp-remote JSON
snippets (default to PROD id), and the prose (distinct per-env clients, how to
read each env's clientid). Doc-only; no code change.
…d-doc

docs(mcp): correct PROD client_id — DEV and PROD have DISTINCT IAS clients
Native-HTTP OAuth MCP clients (Claude Code/Desktop, Joule Work Desktop) bootstrap
via DCR — they POST to a registration_endpoint to get a client_id. The consent
proxy had none (discovery registration_endpoint=null, POST /mcp-oauth/register
404), so the advertised "native HTTP, no bridge" path failed; the only working
path was the mcp-remote bridge with an explicit, env-specific client_id (DEV
0b1e8b56…, PROD 5a7cb5fd…) — the fragility behind the multi-day PROD aud-mismatch
saga.

Add a facade DCR endpoint. /mcp-oauth/register returns THIS environment's existing
tutorials-identity public PKCE client_id, read from the bound
VCAP_SERVICES.identity[].credentials.clientid (resolveIasClientId). That is the
same clientid tutorials-srv-mcp validates token aud against, so native-HTTP clients
get the correct per-env id automatically — eliminating the wrong-env client_id
failure class by construction. No IAS client is created; no secret or
registration_access_token is minted (public client). The client's redirect_uris are
echoed for the client↔proxy leg only (validated at /authorize); the proxy↔IAS leg
always uses our fixed callback. If no identity binding is present, /register returns
500 (never guesses). Gated by the existing MCP_CONSENT_ENABLED flag.

- approuter/lib/mcp-consent.js: REGISTER_PATH, resolveIasClientId(), handleRegister(),
  dispatch + exports
- approuter/lib/well-known-oauth.js: advertise registration_endpoint under the flip
- docs/end-users/mcp-quickstart.md + hugo/content/api-docs/_index.md: native-HTTP/DCR
  path now works (no client_id); reverse the stale "XSUAA does not support DCR" notes;
  mcp-remote + per-env client_id kept as the fallback
- tests: +5 DCR (mcp-consent) +2 discovery (well-known); 60 green
feat(mcp): RFC 7591 Dynamic Client Registration for the consent proxy
Live DEV verification of #2710 (DCR) exposed the bug it was meant to kill,
relocated into the proxy. POST /mcp-oauth/register returned cf2ea5eb… — the
approuter's OWN tutorials-identity clientid — but tutorials-srv-mcp validates
token `aud` against 0b1e8b56… (its separate tutorials-identity instance). A
native-HTTP client registering via DCR would get cf2ea5eb…, complete login, and
hit WrongAudience 401 at /mcp-auth/api — the same aud-mismatch class, just moved.

Root cause: resolveIasClientId() trusted the approuter's VCAP identity binding,
on the (false) assumption from #2593 that the approuter binds the SAME identity
instance as srv-mcp. It does not — verified DEV 2026-10-11:
  - approuter  → instance tutorials-approuter-identity, clientid cf2ea5eb…
  - srv-mcp    → instance tutorials-identity,            clientid 0b1e8b56…
same tenant app_tid (3c6fa3f1…), distinct clients. @sap/xssec kind:ias checks
aud === credentials.clientid exactly, so cf2ea5eb… cannot pass srv-mcp.

Fix: resolveIasClientId() now prefers an explicit MCP_RESOURCE_CLIENT_ID env
(srv-mcp's clientid, pinned per-env in the mtaext — DEV 0b1e8b56…, PROD
5a7cb5fd…), falling back to the VCAP binding only when unset. Deterministic; no
shared-binding assumption. The DCR response is otherwise unchanged (public
client, no secret, redirect_uris echoed for the client↔proxy leg only).

- approuter/lib/mcp-consent.js: MCP_RESOURCE_CLIENT_ID precedence + corrected comment
- deploy/dev.mtaext + deploy/prod.mtaext: pin srv-mcp's per-env clientid
- tests: +3 (precedence over VCAP, /register returns the env value, empty-env
  falls back to VCAP); saveEnv tracks MCP_RESOURCE_CLIENT_ID; 63 green
…-clientid

fix(mcp): DCR must return srv-mcp's clientid, not the approuter's own
@jung-thomas
jung-thomas merged commit 153ddf3 into main Oct 11, 2026
8 checks passed
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.

1 participant