Skip to content

✨ feat: MCP Elicitation Support (URL-Exception -32042 Retry + Form/URL elicitation/create)#69

Closed
TomasPalsson wants to merge 7 commits into
apro-deployfrom
feat/mcp-url-elicitation
Closed

✨ feat: MCP Elicitation Support (URL-Exception -32042 Retry + Form/URL elicitation/create)#69
TomasPalsson wants to merge 7 commits into
apro-deployfrom
feat/mcp-url-elicitation

Conversation

@TomasPalsson

Copy link
Copy Markdown

Summary

Adds full MCP elicitation client support, primarily so AgentCore Gateway MCP_SERVER targets can do per-user 3LO OAuth mid-tool-call (apro tomas sandbox, spec 050):

  • URL-exception mode (MCP 2025-11-25, primary): tools/call rejected with JSON-RPC -32042 (UrlElicitationRequired) now renders an in-chat authorization card (link opens in a new tab) and suspends via FlowStateManager (same pattern as MCP OAuth). "I've authorized — continue" retries the same call exactly once; cancel/10-min timeout fails in-band with the message + URL preserved.
  • Form + URL elicitation/create (2025-06-18/2025-11-25): declares capabilities.elicitation = { form: {}, url: {} }; form requests render typed fields (string/number/boolean/enum), URL requests render the authorization card.
  • POST /api/mcp/elicitation/:flowId completion route with per-user ownership (userId embedded in flow IDs; URI-encoded segments).
  • Token-guarded per-call handler disposal (concurrent calls on a cached connection can't clobber each other's teardown).

Testing

  • packages/api MCP suites: 68 passed (incl. -32042 catch/retry, flow-ID round-trips, disposal guard); full mcp dir 45 suites green pre-rebase
  • client: ElicitationForm 14 + Part 5; SSE/Content dirs 618 passed
  • tsc clean on packages/api, data-provider, client; eslint clean
  • Manual e2e against the tomas AgentCore gateway: pending on this PR (that's the test!)

How to test

  1. Run this branch against the tomas gateway (github-mcp target).
  2. Ask for github-mcp___list_created_prs → expect the authorization card (server must be in ELICITATION_MODE=url).
  3. Open link, consent to GitHub, click continue → the tool call auto-retries and returns real PR data.

Notes

🤖 Generated with Claude Code

…elicitation/create)

Adds MCP elicitation to cover two wire mechanisms needed against real-world
servers, primarily our AgentCore-hosted MCP gateway:

- URL-exception mode (2025-11-25, primary): AgentCore's gateway returns
  JSON-RPC error -32042 (ErrorCode.UrlElicitationRequired) on `tools/call`
  when a tool needs out-of-band user authorization (e.g. a downstream OAuth
  consent screen the gateway can't broker inline). MCPManager.callTool now
  catches this (matching both `McpError`/`UrlElicitationRequiredError`
  instances and raw `{code: -32042}` objects from gateway proxies that don't
  preserve the SDK class), emits an `on_elicitation` SSE event so the chat UI
  renders an authorization card, and suspends via FlowStateManager exactly
  like the existing OAuth "visit a URL, get resumed" flow. When the user
  clicks "I've authorized — continue" (POST /api/mcp/elicitation/:flowId,
  action=complete), the SAME tools/call is retried once and its result
  becomes the tool result. Cancelling or a 10-minute flow timeout fails the
  tool call with the elicitation message + URL folded into the error text,
  so it still surfaces in-band to the model/user.

- Form + URL request mode (2025-06-18/2025-11-25 elicitation/create,
  secondary): declares `capabilities.elicitation = { form: {}, url: {} }` on
  the MCP client and registers a request handler via
  `MCPConnection.setElicitationHandler`, so servers can also drive elicitation
  as a server-initiated request rather than a tools/call exception. Form-mode
  requests render field inputs (string/number/boolean/enum) and resolve with
  action accept/decline; url-mode requests render the same authorization card.

Shared plumbing: a new `mcp_elicit` flow type on the existing
FlowStateManager singleton; a new `POST /api/mcp/elicitation/:flowId` route
with per-user ownership enforcement (elicitation flow IDs embed the userId
directly, unlike OAuth's one-per-server IDs, since elicitation is scoped per
tool invocation); a new `ElicitationContent`/`on_elicitation` content type
and SSE event; an `ElicitationForm` component covering both the form and
URL-card variants; and an `mcpSettings.elicitationMode` config flag forwarded
to the client.

Adapted from a fork patch (form-mode only, against an older LibreChat base)
— rebuilt the MCPManager/connection wiring against the current OBO-aware
`callTool`, and added the URL-exception path and URL elicitation/create
variant from scratch, since neither existed in the fork or upstream.
- Remove the dead mcpSettings.elicitationMode flag: dropped it from
  configSchema and TStartupConfig (data-provider) and from the
  /api/config forwarding block. It had no client consumer; no schema
  test referenced it.
- Fix a per-call elicitation handler leak on cached connections:
  MCPConnection.setElicitationHandler now returns a disposer (removes
  the elicitation/create request handler), and MCPManager.callTool
  clears it in finally so a cached connection can't fire a stale
  closure (old toolName/signal/flow) on a later request.
- Remove two unsafe SDK-boundary casts in the elicitation handler by
  narrowing the params union on params.mode instead of casting.
- Name the handler param union once (ElicitationCreateParams) instead
  of duplicating it in the signature and the cast; derive the removed
  method literal from ElicitRequestSchema (no magic string).
- Centralize the FlowStateManager type-erasure in one audited helper,
  asElicitationFlowManager(), documenting why the singleton's generic
  is erased for elicitation flows; no more scattered `as unknown as`
  double-casts at the call sites.
- URI-encode every variable segment of an elicitation flow ID (userId,
  serverName, toolName — server/tool names are config/user-derived) and
  decode them in parse, so a `:` inside a segment can no longer skew the
  parsed diagnostic fields. Ownership was already sound; parse output is
  now always faithful.
- Add regression tests: the handler is cleared once the tool call
  settles, and flow IDs round-trip (including a colon inside a segment
  and tenant-scoped IDs).
Registration on a shared cached connection is last-writer-wins (the
protocol offers no call correlation for elicitation/create), but an
earlier tools/call settling must not tear down a later call's live
handler: the disposer now only unregisters if its own registration is
still current, and is idempotent.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0423eed5-d1f8-4c38-a111-32acbeea907c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-url-elicitation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TomasPalsson

Copy link
Copy Markdown
Author

CI note: the Tests: api (shard 1/3) failure (strategies/openIdJwtStrategy.spec.js — 2 tests: Entra tenant-issuer validation and preferred_username email fallback) is pre-existing on the base branch: both tests fail identically on bare 650e62089 with none of this PR's changes present (verified locally). All elicitation-related suites pass: @librechat/api 4/4 shards, client Ubuntu 4/4 shards, packages build, smoke boot. 🤖

…rt errors

Observed live against AgentCore Gateway: it returns JSON-RPC errors with
a non-2xx HTTP status, so the SDK's streamable-HTTP transport throws a
StreamableHTTPError (code = HTTP status, body embedded in the message)
instead of an McpError with code -32042 — the elicitation catch never
matched and tool calls failed generically.

extractUrlElicitation() now handles both wire shapes: protocol-level
McpError/UrlElicitationRequiredError, and the HTTP-wrapped body parsed
out of the transport error message. Tests pin the exact observed wire
format, plus non-elicitation and unparseable-body negatives.
@TomasPalsson TomasPalsson marked this pull request as ready for review July 3, 2026 13:59
- extractUrlElicitation: parse embedded JSON-RPC bodies regardless of
  whitespace/key order instead of matching a byte-exact code literal
- connection: do not treat a 401-wrapped -32042 elicitation as an OAuth
  failure; the spurious background reconnect raced the elicitation retry
  and left a stale gateway session (-32600)
- thread serverName/toolName through the on_elicitation payload
- add AgentCore-shaped mock elicitation server (401-wrapped and 200
  JSON-RPC modes, /consent + /reset) and an integration suite driving
  the real client stack through elicit-consent-retry end to end
- shield-headed header with requesting server/tool identity
- staged CTA: one primary action at a time (open page, then confirm)
- footprint-stable per-button loading, error line on failed responses,
  aria-live resolved state, full field-level error ARIA wiring
- consolidate error styling onto semantic destructive tokens
@TomasPalsson

Copy link
Copy Markdown
Author

Superseded by #70, which carries the same feature from a personal fork (head on TomasPalsson/LibreChat) with a cleaned-up 2-commit history and an additional hardening pass (concurrency-safe handler registry, http(s)-only URL validation, per-server config gate, server-side payload validation, localized card, and a full authz test matrix). Closing this one in favor of #70.

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