Skip to content
Latest (currently 2026.8.0) has the newest features, bug fixes, and CVE patches of Solo Enterprise for agentgateway.

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

About elicitations

Page as Markdown

Elicit additional auth info from users for MCP servers.

In the MCP specification, an elicitation is a standardized mechanism for MCP servers to request additional information or action from users through an MCP client, such as the credentials to authenticate with a protected backend API. Solo Enterprise for agentgateway uses this concept of eliciting information from the user to solve a related problem: obtaining and storing a user’s third-party credentials so that the gateway, not the MCP server, holds them.

Solo Enterprise for agentgateway has built-in elicitation support via its token exchange infrastructure. The elicitation instructs the agentgateway proxy to securely obtain and store a user’s third-party credentials on their behalf. When an MCP server needs to access an upstream API that requires its own authentication, such as GitHub, Salesforce, or Google, the agentgateway proxy asks the user to complete an OAuth consent flow. The agentgateway proxy then stores the resulting token and injects it into subsequent upstream requests.

Unlike impersonation or delegation, the token is not created by the STS. Instead, the STS stores the user’s upstream credential and replays it when needed.

Note

The current Solo Enterprise for agentgateway elicitation flow is its own flow, not related to the MCP spec’s URL-style elicitation pattern. When credentials are missing, the gateway returns an HTTP 500 response with a JSON body that contains an authorization URL ({"url":"<elicitation-url>"}). It does not send a structured MCP elicitation/create message to the client. This design is similar in spirit to the URL-style elicitation pattern, but it does not implement the reserved MCP urlMode or formMode message types, and Solo Enterprise for agentgateway configuration intentionally does not use those field names. This way, you can use the MCP spec’s URL-style elicitation pattern or Solo Enterprise for agentgateway elicitations without conflicts.

Why elicitations?

MCP servers often need to call third-party services on behalf of many different users, where each user has their own credentials. Letting the MCP server handle those credentials introduces risk: MCP servers can be developed by different teams, hosted by external vendors, or otherwise outside your control. A credential that transits through an MCP server is a credential that could be logged, leaked, or misused.

Agentgateway solves this by handling the credential exchange out of band in a trusted infrastructure that is separate from the MCP server. The proxy follows these security principles:

  • Third-party credentials never transit through the MCP client or server. The OAuth flow happens out-of-band in the user’s browser.
  • The gateway stores and manages third-party tokens in the STS.
  • Tokens are bound to the user’s identity, not to a session.

Elicitation flows: interactive and brokered

You configure an elicitation with the backend.entElicitation field on an EnterpriseAgentgatewayPolicy or EnterpriseAgentgatewayBackend resource. This field selects exactly one of two flows.

FlowFieldHow the user authorizesUse case
InteractiveentElicitation.interactiveAn administrator or user completes the OAuth consent in the Solo Enterprise for agentgateway UI, which surfaces pending elicitations.You want the Solo Enterprise for agentgateway UI to drive and display the authorization flow.
BrokeredentElicitation.brokeredThe gateway’s built-in OAuth issuer proxy brokers the flow for a protected MCP resource, optionally chaining an upstream OAuth leg (brokered.chainedAuth).You want an MCP-client-driven flow for a protected resource, with no Solo Enterprise for agentgateway UI step.

The interactive and brokered fields are mutually exclusive on a single backend. For the step-by-step interactive setup, see Set up the elicitation infrastructure.

Warning

Migrating from the opaque Secret format: Earlier releases configured the OAuth provider through an opaque Kubernetes Secret referenced by backend.tokenExchange.elicitation.secretName, with keys such as client_id, authorize_url, access_token_url, scopes, and app_id. These values now map to typed fields under backend.entElicitation (interactive.oauth.* or brokered.chainedAuth.oauth.*), and only the client_secret remains in a Secret that you reference with clientSecretRef. The app_id key is removed, and scopes is now a list instead of a space-separated string. The legacy tokenExchange.elicitation.secretName field is still supported as a deprecated fallback, but it is mutually exclusive with entElicitation on the same backend.

Elicitations in agentgateway

When the gateway detects that a user, client, or agent wants to access a backend that requires OAuth authorization, the gateway intercepts the request and triggers an elicitation flow. For the interactive flow, the gateway returns an error to the user with a URL that the user must visit to authorize access. The URL points to the Solo Enterprise for agentgateway UI.

When the user opens the elicitation URL, the user is redirected to the OAuth consent flow in their browser. After the user completes the consent flow, a token is issued to the user and captured by the agentgateway proxy. The gateway keys the token with the user’s identity and the resource the user tries to access, and stores the token in the STS server.

When the user retries the request to the backend, the gateway proxy can now look up the token in the STS server and use this token to successfully authenticate requests to the backend on behalf of the user.

The MCP server is never involved in triggering or completing the elicitation. Only the user who triggered the elicitation can complete it. From the MCP server’s perspective, the server receives a normal request with a valid upstream token that is already injected into the Authorization header. The MCP server has no knowledge of the elicitation process, the identity provider, or the Security Token Service (STS). Note that an administrator cannot complete an elicitation on behalf of another user, because the OAuth consent screen requires the actual user’s browser session.

The following diagram shows the elicitation flow.

    sequenceDiagram
    participant Client as MCP Client
    participant GW as agentgateway
    participant STS as STS
    participant UI as Enterprise UI
    participant Server as MCP Server
    participant Backend as Backend

    alt Token exchange policy configured on backend
        Note over Client,Backend: 1. Token lookup
        Client->>GW: Send request with JWT in <br/>Authorization header
        GW->>STS: Token exchange policy <br/>detected for backend <br/>Send JWT token for token <br/>lookup to STS <br/>(POST /elicitations/oauth2/token <br/>with subject_token=<jwt> <br/>and resource=<backend>)
        STS->>STS: Validate JWT signature against JWKS
        STS->>STS: Extract userId from sub claim
        STS->>STS: Look up stored tokens for userId, resource
        STS->>STS: No token found, create "pending" elicitation
        STS-->>GW: Returns HTTP 400 with elicitation URL
        GW-->>Client: Returns HTTP 500 with <br/>{"url":"<elicitation-url>","status_url":null}
    else No token exchange policy
        GW->>Server: Forward request as-is
    end
    
    Note over Client,Backend: 2. Elicitation authorization via browser
    Client->>UI: User opens elicitation URL in browser
    UI->>Backend: Complete OAuth consent screen <br/>(e.g. GitHub Authorize)
    Backend-->>UI: Redirect to UI with authcode
    UI-->>STS: Forward authcode
    STS->>Backend: Exchange authcode for access token
    Backend-->>STS: Return access token
    STS->>STS: Store access token keyed <br/>by (userId, resource)<br/>and mark elicitation as "completed"

    Note over Client,Backend: 3. Request authorization
    Client->>GW: Retry request with JWT
    GW->>STS: Send JWT token for token lookup 
    STS->>STS: Validate JWT and look up <br/>stored access token
    STS-->>GW: Upstream access token found
    GW->>Server: Inject access token into request <br/>(Authorization header)
    Server->>Backend: Access backend
    Backend-->>Server: Response
    Server-->>GW: Response
    GW-->>Client: Response
  

With elicitations, you can:

  • Request OAuth tokens from external identity providers when they are missing.
  • Manage the lifecycle of credential-gathering flows with states: PENDING, COMPLETED, and FAILED.
  • Securely inject credentials into upstream API calls without exposing them to MCP servers.
  • Support scenarios where upstream APIs require different authentication mechanisms, audiences, or trust domains.

Elicitation-only mode

In elicitation-only mode, the gateway stores the third-party token in the STS but does not inject it into the upstream request. The user’s original IdP JWT is preserved in the Authorization header. Use this mode when you want to track and manage the elicitation lifecycle without replacing the outgoing credential.

Token storage and lifecycle

The agentgateway controller uses a database to store elicitations and tokens. By default, the STS server is set up with a built-in SQLite database. Note that all elicitations and tokens are lost when the controller pod restarts. To persist this information, use an external database instead, such as PostgresQL.

BackendConfigDefault?Persistence
SQLitedatabase.type: "sqlite" (or omit)YesInformation is lost on pod restart
PostgreSQLdatabase.type: "postgres" + database.postgres.urlNoPersists data if backed by a PVC

To configure the database use the following configuration in the controller Helm chart.

# PostgreSQL example
tokenExchange:
  database:
    type: "postgres"
    postgres:
      url: "postgres://user:pass@postgres-host:5432/dbname?sslmode=disable"

Note

The Helm chart currently does not support storing the PostgreSQL credentials in a Kubernetes secret. Instead, you must provide the credentials in plain text as part of the PostgreSQL URL.

Token maintenance

To keep the token exchange database from growing unbounded, the controller can run a periodic background “janitor” that deletes expired, unused tokens and cleans up their related elicitations. The janitor is disabled by default. Enabling is especially useful when you use an external PostgreSQL database that persists records across controller restarts.

The janitor runs as a background process on a fixed interval. Each cleanup cycle does the following:

  • Deletes tokens whose expires_at time passed more than the expiredRetention grace period ago. A successful token refresh resets expires_at, so tokens that are still in active use are never removed.
  • Deletes tokens whose stored data is invalid or orphaned.
  • Resets completed elicitations whose token was deleted back to a pending state, so the next request triggers a fresh authorization flow.
  • Deletes Pending or Failed elicitations that have not been updated within the elicitationRetention grace period.

On PostgreSQL, the janitor uses leader election so that only one controller replica runs cleanup at a time. On SQLite, each pod has its own database file, so every replica cleans up its own file.

To enable and tune the janitor, add a maintenance block to the tokenExchange configuration in the controller Helm chart. The janitor requires tokenExchange.enabled: true.

tokenExchange:
  enabled: true
  maintenance:
    enabled: true
    interval: 1h
    expiredRetention: 720h
    elicitationRetention: 1440h
    cycleTimeout: 30s
FieldDescriptionDefault
maintenance.enabledEnable the periodic token and elicitation janitor. Requires tokenExchange.enabled: true.false
maintenance.intervalPeriod between cleanup cycles. Minimum 1m.1h
maintenance.expiredRetentionGrace period after a token’s expires_at time before the token is eligible for deletion. Minimum 24h.720h (30 days)
maintenance.elicitationRetentionGrace period after an elicitation’s last update before a Pending or Failed row is permanently deleted. Must be greater than or equal to expiredRetention.2 × expiredRetention (1440h / 60 days)
maintenance.cycleTimeoutMaximum duration for a single cleanup cycle, which prevents a slow database from stalling the janitor.30s

Known limitations

The controller does not validate that the upstream provider returned a non-empty access_token. If the provider returns {"access_token": ""}, which GitHub does when the redirect_uri doesn’t match, the empty token is stored and the elicitation is marked completed. Subsequent requests fail with a 400 HTTP response code from the backend. You must delete the elicitation and redo the OAuth flow to solve this issue.

Next steps