Repository navigation
feat(adapters,plugins): actions a server names by method, tag or id ask the user before they run - #1099
Merged
frontegg-david merged 6 commits intoNov 4, 2026
Conversation
…k the user before their request A model cleaning up test invoices can write a script that calls deleteInvoice twelve times, and every request went out with nobody asked. The new confirm option names the actions that need the user's yes: by HTTP method, by a tag of the operation or of a skill that lists it, or by operationId. Such an action asks through an MCP elicitation after it is authorized and its input checked, and before its request; a decline sends nothing and fails the call with "the user declined: <actionId>", audited as a failed call. A workflow asks once, before its script starts, about the actions it names: waiting for the user inside the script would count against its 8 second limit. A client that can't show the question is refused, or with confirm.whenUnsupported: 'allow' runs the action unasked. Without the option nothing asks, as before. Compiled bundles now keep each operation's OpenAPI and x-frontmcp tags, and the strict bundle schema accepts them. Operation tools are class-based tools, so each can ask the user through its own elicit.
…/skilled-confirm-destructive # Conflicts: # plugins/plugin-skilled-openapi/src/tools/run-workflow.tool.ts
…ends nothing, and the CLI refuses it Integration tests run the confirm option through a real server and an in-process client with and without elicitation: execute_action, operation tools and run_workflow ask, a workflow that voids twelve invoices asks once, a decline sends nothing and is audited as a failed call, and a server with no confirm option never asks. The production-settings e2e app gets a server whose DELETE actions ask, tested over streamable HTTP with a client that answers the question and one that can't. The CLI e2e fixture gets a DELETE action that asks: the generated CLI, which can't show the question, exits 1 for it in-process and through its daemon. The plugin README and the skills catalog describe the option.
The option and the confirmation helpers had multi-line doc comments; the README describes them in full.
frontegg-david
marked this pull request as ready for review
November 4, 2026 20:39
Contributor
Author
|
@coderabbitai review |
Contributor
|
Warning Review limit reachedYour organization has reached its usage spending cap. Adjust your spending cap in the billing tab. Next included review available in 9 minutes. View limit details
|
Contributor
Action not completed
|
Contributor
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-04T22:09:38.817Z |
alexmercerpo
approved these changes
Nov 4, 2026
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.
What and why
Say a user asks "clean up the test invoices", and the model writes a script that calls
deleteInvoicetwelve times. Until now all twelve requests went out, and nobody asked the user. This PR adds aconfirmoption to the Skilled OpenAPI plugin. Actions it names ask the user, through an MCP elicitation, before their request is sent.It is opt-in: a server that sets nothing behaves as before, so developers can keep running destructive actions without being asked.
This PR stacks on #1098 (saas e2e), which stacks on #1097 (
execute_action). It merges #1098 in. #1098 changedrun_workflowto resolve actions from a registry snapshot taken when the workflow starts. The confirmation uses that same snapshot, so the question and the script see the same bundle.Which actions ask
methodis an error, not a rule that silently matches nothing.compileSkilledBundleFromOpenApi()keeps each operation's OpenAPItagsand itsx-frontmcp.tags, each once.operations.*.tags: up to 64 tags of up to 256 characters each.When it asks, and what
execute_action, or an operation tool that server code calls) asks inside the shared executor:Run voidInvoice: Void an invoice (DELETE /invoices/{id}) with {"id":"inv_1"}?.{ confirmed: boolean }field, the pattern the elicitation docs use.load_skilljudges them).execute_action.callTool, so a user taking longer than that to answer would kill the workflow.confirmed: false) sends nothing:isError: trueandthe user declined: voidInvoice;the user did not answer in time: voidInvoice.whenUnsupported: 'refuse', the default, the action is refused:the user's confirmation is needed for voidInvoice, and this client can't ask for it: it doesn't support elicitation; nothing was sent;this server can't ask for it: elicitation is disabledinstead;'allow', the action runs without asking.ElicitationFallbackRequired,ElicitationNotSupportedError,MissingClientCapabilityErrorandElicitationDisabledErroritself. It does not use the SDK's LLM-mediated elicitation fallback: with that fallback the model answers the question, so it would be confirming its own call.InputRequiredSignal) passes through untouched, so that round trip still works.authority-check-pass, thenhttp-call-failurewith status 0 and the refusal as itserrorMessage;How it's built
this.elicit, so theelicitation:requestandelicitation:resultflows and their hooks run for it. It runs inside the tool call, after the plugin's own authorization and validation. Atools:call-toolhook would run before those, so the confirmation is a step of the shared executor instead.ToolContext.elicitis protected, and operation tools were FUNCTION-kind tools whose executor got the context from outside. They are now one generatedToolContextsubclass per operation (CLASS_TOKEN records), so each asks through its ownthis.elicit. Their names, metadata, visibility, hooks and answers are unchanged.The generated CLI. It can't show the question:
onElicitationhandler;capabilities: {}.So
execute-actionandrun-workflowexit 1 for such an action, with the refusal above, and send nothing. A CLI e2e test checks both paths. A terminal prompt and a--yesflag are left for the PR that reworks the CLI commands. The daemon's JSON transport would need server-to-client requests for that, which is not a small change. Until then, a server that wants CLI users to run such actions setswhenUnsupported: 'allow'. That setting applies to every client without elicitation, not only the CLI.Model-facing text. The
execute_actiondescription now says an action may ask the user first, and liststhe user declined: <actionId>among its failures (933 characters, within the 1024 budget). Therun_workflowdescription is unchanged: it was already exactly 1024 characters. A workflow's refusals tell the model what to do.User-facing change
confirm: { methods?, tags?, operations?, whenUnsupported?: 'refuse' | 'allow' }. The actions it names ask the user, through an MCP elicitation, before their request. It needselicitation: { enabled: true }on the server.the user declined: <actionId>,the user did not answer in time: <actionId>,the user's confirmation is needed for <actionId>, and this client can't ask for it: …; nothing was sent, and therun_workflowrefusal for an action its question didn't name.compileSkilledBundleFromOpenApi()emitsoperations.*.tags, and the bundle schema accepts them.this.callTool('<bundleId>.<operationId>')behaves as before.ConfirmOptions.confirmoption: unchanged, nothing asks.confirm: matching actions ask first. A decline sends nothing and fails the call. A client without elicitation, the generated CLI included, is refused unlesswhenUnsupported: 'allow'.confirm).frontmcp-config/references/configure-elicitation.md: new "Confirming Skilled OpenAPI Actions" section;frontmcp-extensibility/references/skill-audit-log.md: how a declined call is recorded;frontmcp-deployment/references/build-for-cli.md: the CLI refuses such an action;frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md: managed mode has no confirmation.Tests
Added:
@frontmcp/adapters, 3 tests:x-frontmcptags, each once, and an operation without tags has none;action-confirmation.spec.ts(23 tests):confirmed: false, and a timeout;refuseand withallow;InputRequiredSignalis rethrown;execute-skill-action.spec.ts(+5 tests):http-call-failure;confirm-actions.integration.spec.ts(13 tests), with a real server andconnect(), with and withoutonElicitation:execute_actionasks and sends on accept, and sends nothing on decline;execute_actionwithout operation tools asks;allowthe action runs;confirmnever asks.operation-tool.factory.spec.ts: the executor tests now build the class-based tool, as the call-tool flow does.demo-e2e-skilled-openapi, production settings over streamable HTTP, new server entrypoint withconfirm: { methods: ['DELETE'] }and elicitation enabled:confirm-destructive.e2e.spec.ts(6 tests): the DELETE asks and is sent on accept, nothing is sent on decline, a three-invoice workflow asks once before any request, a declined workflow sends nothing, a client without the elicitation capability is refused, and an unmatched action doesn't ask;execution.e2e.spec.ts(+1 test): the server withoutconfirmvoids an invoice byexecute_actionand in a workflow without asking.demo-e2e-cli-exec,cli-skilled-openapi.e2e.spec.ts(+2 tests): the generated CLI'sexecute-actionfor a DELETE that needs confirmation exits 1 with the refusal and sends nothing, in-process and through the daemon. The shared fixture gains avoidInvoiceaction and theconfirmrule.Run:
npx nx run-many -t build -p sdk,adapters,plugin-skilled-openapi,testing,cli: passed.npx nx run-many -t typecheck -p adapters,plugin-skilled-openapi,demo-e2e-skilled-openapi,demo-e2e-cli-exec: passed.libs/adaptersjest: 61 suites, 1070 tests passed.plugins/plugin-skilled-openapijest: 46 suites, 679 tests passed.libs/skillsjest: 3 suites, 249 tests passed.demo-e2e-skilled-openapie2e: 8 suites, 80 tests passed, the saas specs from test(e2e): the saas bundle source on Node, and a workflow finishes on the operations it started on #1098 included.demo-e2e-cli-exec:cli-skilled-openapi.e2e.spec.ts16 tests passed.cli-login-authorities.e2e.spec.ts(4 tests) uses the changed fixture, but it is skipped on macOS by design (it would write to the Keychain), so it runs only in CI.prettier --checkandeslinton the changed files, andscripts/fix-unused-imports.mjs: clean.libs/sdk, so the browser bundle-size e2e doesn't apply.