✨ feat: Support MCP Elicitation (URL + Form) for Deferred Auth in Tool Calls#71
Draft
TomasPalsson wants to merge 10 commits into
Draft
✨ feat: Support MCP Elicitation (URL + Form) for Deferred Auth in Tool Calls#71TomasPalsson wants to merge 10 commits into
TomasPalsson wants to merge 10 commits into
Conversation
Surface MCP server-initiated elicitation during tool calls. When a tools/call returns JSON-RPC -32042 (UrlElicitationRequired, spec 2025-11-25) or the server sends elicitation/create, LibreChat holds the call open, prompts the user, and resolves/retries once they respond. Primary driver: AWS Bedrock AgentCore Gateway, which defers per-user OAuth to a browser flow and signals it via -32042. Backend: - Detect and extract -32042 from JSON-RPC error objects, SDK McpError, and HTTP-wrapped transport errors; accept only http(s) auth URLs (reject javascript:/data:/vbscript:) in both wire shapes. - Per-connection elicitation handler registry behind one stable dispatcher, so concurrent tool calls on a shared connection cannot orphan or misattribute each other's elicitation/create. - Cap concurrent pending elicitations per (user, server); tie the tool-call timeout to the flow TTL; thread the SDK abort signal so a server cancellation or connection close tears down the pending wait. - Gate per-server via an `elicitation` config flag (enabled by default); validate submitted content against the requested schema server-side; strip elicitation parts from the model-bound payload. - Emit on_elicitation and on_elicitation_resolved SSE events; add data-provider types, endpoints, and the completion route with per-user flow-ownership authorization. Covered by unit and integration tests for detection, single-retry semantics, the handler registry, the config gate, and the route authz matrix.
Render an in-chat card for MCP elicitation: an authorization link for URL mode and a validated form (string/number/boolean/enum) for form mode, each with accept/decline/cancel. On the -32042 flow the user opens the server-provided auth URL and the tool call auto-retries after they confirm. - Render only http(s) auth URLs; show a warning instead of a link for javascript:/data:/vbscript: and keep Continue disabled. - Localize every validation message via useLocalize with interpolation; guard numeric fields against NaN and omit empty optional fields. - Announce resolved status through a persistent sr-only aria-live region, matching the sibling tool-call cards. - Consume the on_elicitation_resolved SSE event and patch the resolved card in place so it survives re-render. Covered by ElicitationForm and useStepHandler unit tests, including the unsafe-URL and NaN-guard paths.
Close the remaining gaps against the authoritative MCP 2025-11-25
elicitation spec (form + URL mode), verified by an audit of the spec's
client-side MUST/SHOULD clauses.
URL-mode client security (client/ElicitationForm):
- Show the full authorization URL as visible text for examination before
the user consents to open it (spec MUST), not just an "Open" button.
- Highlight the destination host/domain, matching the existing OAuth
sign-in card treatment (subdomain-spoofing mitigation).
- Warn on Punycode / mixed-script (homograph) hostnames.
Form-mode requestedSchema subset — render + validate the full set:
- string `pattern` (regex) and `format` (email/uri/date/date-time,
mapped to native input types);
- `oneOf: [{const, title}]` titled enums and `enumNames` display titles;
- multi-select `type: "array"` (items enum/anyOf, minItems/maxItems),
emitted as a real JSON array in the response payload.
Server-side (routes/mcp.js) validates the same subset — pattern, format,
oneOf membership, and array membership/bounds — so client validation
cannot be bypassed by a direct API call.
Also retain the server-supplied `elicitationId` alongside the flow id
(kept internal) instead of discarding it.
Known follow-ups (documented, not addressed here): negotiated
protocol-version gating for URL mode, a -32602 override for malformed
mode values (currently the SDK default -32603), and a
notifications/elicitation/complete auto-retry handler (spec MAY).
Covered by new client and server tests (email/uri/date formats, pattern,
oneOf, multi-select array, full-URL display, domain + Punycode warning,
and the server validation matrix).
Backport of the review fixes from the upstream URL-mode slice (danny-avila#14141) to the full implementation: - Gate the advertised elicitation capability on the per-server `elicitation` flag, so an opted-out server (`elicitation: false`) isn't told the client supports elicitation. - Apply the per-(user, server) MAX_PENDING_ELICITATIONS cap to the -32042 URL-exception path too (previously only the elicitation/create path was capped), preventing unbounded concurrent flows. - Give stripUiOnlyContentParts type-honest overloads so the non-array pass-through isn't dead code under the T[] signature. - Correct the elicitationFlowContext eviction comment to match the size-bounded, TTL-swept-when-over-cap behavior.
Once an elicitation resolves, render a muted single-line confirmation at the sibling tool call's "Completed …" altitude instead of a persistent full-width card. Mirrors the in-ToolCall OAuth sign-in, which leaves no lingering confirmation once the call proceeds; kept (not removed) so a cancel/decline stays legible and the transcript keeps a breadcrumb.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
8 tasks
- Refuse URL elicitation on the shared app-level connection, so a gateway that binds auth to the MCP session can't leave it authorized as one user for everyone else (Codex P1). - Reject the loser of a concurrent completion: completeFlow returns true even for an already-completed flow, so re-read and 409 if another action won the race, instead of emitting a second resolution (Codex P3).
Pending URL-mode elicitation cards are UI-only events, so they were never reconstructed on resume — a refresh during a pending authorization lost the card until the flow timed out. Persist on_elicitation/on_elicitation_resolved via the same replay-event path OAuth cards use (deduped by flowId), so a resumed client re-renders the card and can still complete or cancel it. (cherry picked from commit f50b363)
…ction The app-level connection guard now refuses URL elicitation on the shared app connection, so these tests must model the user-scoped connection the real flow uses (empty app connections + getUserConnection).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds client + server support for MCP elicitation — the mechanism by which an MCP server pauses a tool call to ask the user for something before it can proceed. LibreChat now handles both wire mechanisms from the 2025-11-25 MCP spec:
tools/callreturns the JSON-RPC error-32042(UrlElicitationRequired), or the server sends anelicitation/createrequest withmode: "url". LibreChat surfaces an in-chat authorization card; the user opens the server-provided URL (e.g. a browser OAuth flow) and, once they confirm, the originaltools/callis retried automatically — exactly once.elicitation/createrequest carrying arequestedSchema. LibreChat renders a validated form (string / number / boolean / enum) and returns the user'saccept/decline/cancelresponse to the server.Motivation / root cause. MCP servers that front delegated auth — notably AWS Bedrock AgentCore Gateway, which defers per-user OAuth to a browser flow — signal "the user has to authorize in a browser first" via
-32042. Today LibreChat treats that as a hard tool failure, so those servers are unusable through the gateway. This change makes the deferral a first-class, resumable interaction instead of an error.Targets
apro-deploy. This supersedes #69 (same work, but with the branch on the org repo); #69 will be closed. No upstream issue is linked because this lands on our internal integration branch, notdanny-avila/LibreChat.Change Type
Testing
Automated — all green locally:
packages/api/src/mcp/__tests__/elicitation.test.ts(29) —-32042extraction across JSON-RPC / SDKMcpError/ HTTP-wrapped transport shapes,http(s)-only URL allow-listing (rejectsjavascript:/data:/vbscript:), the per-connection handler registry, and the abort-signal path.packages/api/src/mcp/__tests__/MCPManager.test.ts(62) — single-retry semantics, the concurrent-pending cap, timeout tie-off, andrequestedSchemaflow-metadata threading.api/server/routes/__tests__/mcp.spec.js+api/server/services/__tests__/MCP.spec.js(145) — thePOST /api/mcp/elicitation/:flowIdauthorization matrix (401 / 400 bad-action / 400 schema-violation / 403 cross-user / 404 / 200), theon_elicitation/on_elicitation_resolvedSSE emitters, and the config gate.client/.../__tests__/ElicitationForm.test.tsx(19) +client/src/hooks/SSE/__tests__/useStepHandler.spec.ts(65) — card rendering, the unsafe-URL and NaN-guard paths, localized validation, and the resolution write-back handler.packages/api/src/mcp/__tests__/urlElicitation.integration.test.ts(12) — end-to-end retry against a real MCP SDK stack (local only; excluded from CI by the repo's*integration*ignore pattern).Reproduce:
tsc --noEmitis clean onpackages/apiandclient; ESLint/Prettier clean on all touched files.Manual — verified end-to-end against an AWS Bedrock AgentCore Gateway target: the card renders, the auth link opens, the tool call auto-retries and succeeds after confirmation, and a declined/cancelled flow surfaces a clean error with no retry loop.
Test Configuration:
backend-review/frontend-review/eslint-cijobs.-32042(packages/api/src/mcp/__tests__/helpers/).Checklist
elicitationserver flag is documented inlibrechat.example.yaml; the external librechat.ai docs are not updated (internal branch).Notes for reviewers
elicitationflag (in the MCP server config schema, documented inlibrechat.example.yaml), enabled by default; setelicitation: falseon a server to disable it — no code change or restart-with-revert needed.http(s)only, both server-side (extraction) and client-side (render), so a compromised MCP server can't injectjavascript:/data:URIs. The completion route isrequireJwtAuth+ per-user flow-ownership checked (unguessable UUIDflowId), and user-submitted form content is validated server-side against therequestedSchemabefore it reaches the server — client-side validation is not trusted.elicitation/create. Pending elicitations are capped per (user, server).on_elicitation_resolved), but is not re-materialized after a full page reload, because the canonical content aggregator lives in the@librechat/agentspackage (out of scope for this PR). This matches the existing behavior of OAuth sign-in cards. Deferred intentionally.a11yand general*integration*jobs are gated to thedanny-avila/LibreChatupstream repo / excluded by pattern, so they don't execute on this fork PR. Accessibility (persistentsr-onlyaria-liveregion, focus handling) was reviewed manually against the sibling tool-call cards.