MastertheMesh
agentgateway · MCP · overview
Overview

MCP Elicitation for upstream OAuth

TO
Tom O'Rourke
EMEA Field CTO · Solo.io
Credit to Ram Vennam. The lab this page summarises is Ram's Snowflake-via-Entra workshop gist — the end-to-end YAML, Helm values and Claude Code wire-up that get a real elicitation flow running. Ram is US Field CTO at Solo.io and the author behind github.com/rvennam. This page is the conceptual companion: what elicitation is, which actors and components show up, and how the identities line up. For the apply-and-run path, head to the gist.

Elicitation is how Enterprise AgentGateway bridges a gap that shows up the moment you put an MCP server behind a corporate IdP: the user has authenticated to the gateway with a token from one IdP, but the upstream MCP server (Snowflake managed MCP in the source workshop) will only accept tokens from a different, upstream IdP. The gateway can't mint that upstream token on the user's behalf because the upstream IdP doesn't trust the inbound one. Elicitation collects the missing consent just-in-time, stores the resulting access + refresh tokens keyed to the inbound user, and replays them on every subsequent call. This page covers the actors and components, the three identities that have to line up, the flow, and where elicitation fits versus other multi-leg auth patterns.

MCP Enterprise AgentGateway Elicitation OAuth 2.0 Two IdPs Keycloak Microsoft Entra ID Snowflake External OAuth offline_access · refresh per-user upstream identity

The lab Ram wrote up is concrete: an MCP client (Claude Code) calls Snowflake's managed MCP server through Enterprise AgentGateway. Inbound the user signs in with Keycloak. Upstream Snowflake will only accept a token from Microsoft Entra ID via its External OAuth integration. Two different IdPs, no federation between them, and a real per-user identity required at the database end so existing Snowflake RBAC keeps working. Elicitation is the pattern that closes the gap. This page sits one layer above the YAML and describes the moving parts; the gist itself walks the apply path.

§1 The two-IdP gap elicitation closes

The inbound and the upstream IdPs don't have to be the same and usually aren't. The corporate IdP issues the user's day-to-day SSO; the upstream SaaS sits behind its own OAuth surface, often a different tenant entirely. The gateway sees both but can't mint a token in the upstream IdP's name without a one-time user consent.

Leg Who decides identity What the gateway has by default
Inbound — MCP client → gateway Corporate IdP (Keycloak in the lab). User signs in once, JWT carries sub, aud, iss. A validated Keycloak JWT, audience + issuer pinned at the gateway.
Upstream — gateway → managed MCP server Upstream IdP (Microsoft Entra in the lab). Resource accepts only tokens issued by this IdP. Nothing. The Keycloak token is the wrong issuer, wrong audience, wrong signing keys. Snowflake will reject it.

The gateway needs a real Entra token for the calling user. Asking that user to paste an Entra token by hand defeats the point. Asking them to log in to Entra from inside the MCP client doesn't work either — the client speaks MCP, not browser-OAuth. Elicitation moves the consent out of the client and into a separate browser flow the user runs once, then the gateway holds the result.

§2 What elicitation actually is

Strip away the actors and elicitation is a small protocol with three moves: pause, prompt, persist. The gateway pauses an MCP call that can't yet be served, prompts the user out of band for the upstream credential, and persists the result so the next call is unblocked.

Net-net. Elicitation is OAuth's authorisation-code flow run on behalf of the gateway, with the gateway acting as the OAuth client to the upstream IdP. The only novel piece is the pause: the gateway uses the in-flight MCP call to signal "I need consent" instead of forcing the user to remember to authorise before they call anything.

§3 Actors and components

Six moving parts, three of them human-facing. The gist's worked example pins each role to a concrete product; the pattern is general.

Component Role In the gist
MCP client Holds the inbound JWT, sends MCP calls to the gateway, displays the elicitation URL when the gateway asks for one. Claude Code, connected via claude mcp add --transport http.
Inbound IdP Authenticates the user. Issues the JWT the gateway validates on every MCP call. Supplies the sub that the gateway uses as the elicitation key. Keycloak realm snow with agentgateway, solo-ui and solo-ui-frontend clients. Anonymous Dynamic Client Registration on so the MCP client can self-register.
Enterprise AgentGateway Validates inbound JWTs, owns the elicitation state machine, acts as the OAuth client to the upstream IdP, stores access + refresh tokens, injects the upstream token on outbound MCP calls and rotates it automatically. EnterpriseAgentgatewayPolicy for inbound JWT + CORS, EnterpriseAgentgatewayBackend for the Snowflake target, an elicitation policy on that backend, and an elicitation-secret holding the upstream OAuth client credentials.
Solo Enterprise UI Where the user lands when they open the elicitation URL. Sits in front of the gateway's elicitation API, gates it with the same inbound IdP, and renders the "Authorize" screen that kicks off the upstream OAuth redirect. Solo management UI, configured against the same Keycloak realm and clients solo-ui / solo-ui-frontend.
Upstream IdP Issues the token the resource will actually accept. Owns its own users, scopes and consent. The gateway is registered here as a confidential OAuth client. Microsoft Entra ID app registration with session:role-any scope, web redirect pointed at the gateway's elicitation callback, client secret stored in the gateway's elicitation-secret.
Upstream resource The thing being protected. Configured to trust the upstream IdP and to map a claim in the upstream token onto its own user identity. Snowflake managed MCP. EXTERNAL_OAUTH security integration trusts Entra, maps upn → Snowflake LOGIN_NAME, runs queries under the user's DEFAULT_ROLE.

§4 The three identities

The user has one experience but the system carries three distinct identities. Each lives in a different system, each does a specific job, and they have to line up — this is where most first-time setups go wrong.

Identity Lives in Job
Inbound user Inbound IdP (Keycloak) Authenticates the MCP call. Authenticates the Solo UI session. The sub claim is the key the gateway files the upstream token under.
Upstream user Upstream IdP (Entra) Consents on the upstream IdP's screen. Carries the claim (upn in the gist) that the upstream resource will use as its user-mapping attribute.
Resource user Upstream resource (Snowflake) Runs the actual work. Picks up the user's roles, warehouse defaults, grants. Mapped from the upstream token's user-identifier claim.

§5 How the identities link

Two joins. Get either wrong and the second leg fails silently with "no token found" or "user not provisioned".

Worth contrasting with token exchange — carefully. Conceptually elicitation and RFC 8693 token exchange are different patterns: elicitation collects browser-driven consent across independent IdPs; RFC 8693 silently mints a new token within a shared trust domain. Where the inbound and upstream sides do trust the same IdP (or have a federation in place), token exchange skips the browser entirely. See the lab pages JWT token exchange and JWT, OIDC and on-behalf-of in Istio Ambient for that pattern.

Implementation note. Solo files elicitation inside the token-exchange machinery in the CRD shape — the policy lives under spec.backend.tokenExchange.elicitation, the Helm keys sit on tokenExchange.elicitation.*, and the data-plane STS endpoint serves the elicitation OAuth dance at /elicitations/oauth2/token. So when you read the gist and see a policy called snowflake-mcp-tx, that -tx suffix isn't an error: it's the cross-IdP-consent mode of the token-exchange subsystem, not same-domain RFC 8693 exchange.

§6 Architecture at a glance

The gateway sits between the MCP client and the upstream resource, talking to two IdPs and one UI. Solid arrows are call paths, dashed arrows are browser redirects the user follows manually during the one-time consent.

MCP CLIENT Claude Code holds inbound JWT SOLO ENTERPRISE UI elicitation host where the user clicks Authorize ENTERPRISE AGENTGATEWAY Inbound JWT validator issuer + audience pinned, JWKS pulled Elicitation broker + token store keyed by inbound sub, access + refresh Upstream token injector replays Entra access token per call INBOUND IdP Keycloak corporate SSO, JWKS UPSTREAM IdP Microsoft Entra ID issues the token the resource accepts UPSTREAM RESOURCE Snowflake managed MCP External OAuth, upn → LOGIN_NAME MCP + JWT JWKS code → token, refresh MCP + Entra token Solo UI sign-in via inbound IdP Browser consent at upstream IdP Authorize click call path user-driven browser flow (one-time)

Components and the two distinct paths: gateway-to-machine calls and one-time browser consent.

§7 Request flow, first call vs steady state

The interesting moment is the first call from a given user. Everything after that looks like a normal MCP call — the gateway pulls the cached upstream token out of its store and rotates it via refresh when it's about to expire.

STEP 0 / 10 Press Next to begin
MCP CLIENT Claude Code AGENTGATEWAY OAuth client SOLO UI consent host UPSTREAM IdP Entra RESOURCE Snowflake MCP ① MCP call (Bearer = inbound JWT) ② HTTP 500 + elicitation URL ③ user opens URL, signs in to Solo UI ④ redirect to /authorize ⑤ consent → auth code to callback ⑥ POST /token (code + client_secret) ⑦ access_token + refresh_token (offline_access) stored · key = sub + resource ⑧ retry MCP call (same inbound JWT) ⑨ MCP call + injected upstream token (upn → resource user) ⑩ tool result, all the way back

What's on the wire

No request in flight yet.

Gateway token store — for this user

Nothing stored for this user yet.

The first call from a given user is the interesting one. Click through to watch the gateway pause the MCP call, prompt the user for upstream consent, and persist the resulting tokens — after which every call looks ordinary.

Steps 1–7 happen once per user. From step 8 onward, every call looks ordinary and the gateway refreshes the upstream token in the background as it nears expiry.

§8 When elicitation fits, when it doesn't

Elicitation is the right tool when the inbound and upstream IdPs are genuinely independent and the upstream side won't accept anything minted on its behalf. Other patterns fit other shapes — the point is to pick by the trust topology, not by what's familiar.

Reach for elicitation when

  • The corporate IdP and the upstream provider's IdP are different and have no federation.
  • The upstream resource enforces per-user identity and existing RBAC over there should keep working (Snowflake roles, Atlassian permissions, GitLab scopes).
  • Users would otherwise have to manage long-lived upstream tokens by hand.
  • You're happy with a one-time browser consent per user, with refresh-token rotation thereafter.

§9 Anatomy of the configuration

Enough to recognise the moving parts in the gist. The full YAML, Helm values, Snowflake SQL and Claude Code wire-up are in Ram's workshop gist — this page intentionally doesn't repeat them.

§10 Things that bite first time

Things to budget for when you stand this up. Each is a place I'd expect a brand-new setup to land on at least once.

Where to go next. For the apply-and-run walkthrough — Keycloak realm JSON, Entra app registration, Snowflake SQL, gateway CRDs, Solo UI values and Claude Code connection — head to Ram's Snowflake + Entra workshop gist. For broader MCP-auth context, the field guide Securing MCP and agentic systems covers the three-hop authn model that elicitation fits inside, and RFCs for JWT, OAuth and MCP-auth indexes the underlying standards.

Thanks again to Ram Vennam for the source workshop and for the patient walk-throughs while I'm ramping on Solo's MCP stack.