✨ feat: Support MCP Elicitation (URL + Form) for Deferred Auth in Tool Calls#70
✨ feat: Support MCP Elicitation (URL + Form) for Deferred Auth in Tool Calls#70TomasPalsson wants to merge 6 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.
|
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 |
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.
|
Superseded by #71 — same branch/content, but hosted intra-fork (aproorg → aproorg:apro-deploy) instead of cross-fork from a personal fork. Continuing there. |
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.