Repository navigation
feat(sdk,plugins): a failed action shows the API's error body, callTool takes { throwOnError: false }, and a failed workflow is an error result - #1095
Merged
Conversation
…chema The tools/call flow already sends a CallToolResult a tool returns as it is, whatever its outputSchema. The decorator's return-type check refused one on a tool with an outputSchema, so such a tool could not report a failure as an isError result that keeps its structured content without a cast.
…y, and callTool takes { throwOnError: false }
A 4xx/5xx answer reached the script as `action "x" failed with status 404`,
so the model could not see what the API objected to, and one failed call
ended the script, which has no try/catch. Now:
- the thrown error ends with the API's error body: JSON compacted, text as
it is, cut at 2 KB with an ellipsis, and run through redactJson with the
field the operation's credential travels in;
- outbound.errorBodies: 'status-only' keeps the status-only message;
- callTool(actionId, input, { throwOnError: false }) returns
{ ok, status, data } (status 0 and an error for a failure with no answer).
The interpreter passes its tool handler two arguments, so a callTool
declared ahead of the program folds the options into the input under a
reserved key;
- a failed workflow is an isError result with the same structured content,
so MCP clients see a failed call and the generated CLI exits 1.
The run_workflow description says so, within the 1024-character budget.
… on a 404, and a failed run-workflow exits 1 Over MCP against the production-settings billing API, whose refund endpoint now answers 422 with a JSON body when a refund exceeds the paid amount; and on a CLI built with the plugin, run-workflow exits 0 for a workflow that succeeds and 1 for one that fails.
…skilled-error-bodies
… try/catch in AgentScript The CLI page says what a non-zero run-workflow exit means, and the Cloudflare skills-only page points a script that wants try/catch at throwOnError: false.
frontegg-david
marked this pull request as ready for review
November 4, 2026 19:31
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 27 minutes. View limit details
|
Contributor
Action not completed
|
alexmercerpo
approved these changes
Nov 4, 2026
Contributor
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-04T20:04:57.572Z |
This was referenced Nov 4, 2026
Merged
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
When a
run_workflowcall failed, the model couldn't see why, and a script couldn't react:action "getInvoice" failed with status 404. The API's error body, such as{"error":"refund_too_large","message":"Refund 500 exceeds the paid amount 120.","maxRefund":120}, was dropped. The result envelope already kept it; therun_workflowbridge dropped it.try/catch.run_workflowanswered{ success: false, error }withoutisError. MCP clients saw a successful tool call, and the generated CLI'srun-workflowexited 0.This PR makes failures something the model and scripts can act on.
The error carries the API's error body. A failed action's error is now
action "<id>" failed with status <N>: <body>.JSON.stringify. A text body is kept as it is. An empty body adds nothing.MAX_ERROR_BODY_LENGTH), and a cut ends with….redactJsonfrom@frontmcp/utils, the redaction the SDK's call recorder uses. It removesBearer/Basiccredentials, JWTs and URL credentials, plus the field the operation's credential travels in (authorization, or an apiKey binding's header or query name). The plugin had no header/token redaction of its own to reuse for this.outbound.errorBodies: 'include' | 'status-only', default'include'. It's validated by the Zod options schema.'status-only'keeps the status-only message, for APIs whose error bodies hold internal details.callTool(actionId, input, { throwOnError: false })returns{ ok, status, data }instead of throwing. The default still throws.ok: true, the status and the data.ok: false, the status, and the redacted, capped body asdata. A body over the cap comes as its cut text. With'status-only'there's nodata.ok: false,status: 0and anerrorstring.@enclave-vm/core/worker'sInterpreterAdapter(2.17.0, the latest) calls the tool handler with two arguments only (toolHandler(name, args)), so a thirdcallToolargument never reaches it. The tool now runs a shortcallTool(actionId, input, options)function ahead of the script. It folds the options into the input under a reserved key,__callToolOptions, and the handler strips that key before validation. The script is transformed withadditionalIdentifiers: ['callTool'], the way the fullEnclavekeeps its own globals, so script calls reach that function. A failed call still throws from the host, since the interpreter has nothrowstatement. Once the interpreter passes a third argument, this function can go.A failed workflow is an error result. When
successisfalse,run_workflowanswers aCallToolResultwithisError: true.structuredContent(success,error,stats), and its text content is that JSON, as for a success.run-workflowexits 1, through the CLI's existingisErrorhandling.tools/callflow already sent aCallToolResulta tool returns as it is. But@Tool's return-type check refused that return type on a tool with anoutputSchema. The check now acceptsOut | CallToolResult. It's a type-only change inlibs/sdk, with a type test.Description.
run_workflow's description now coversthrowOnError, the error body and the error result. It's shorter: 1008 characters, within the 1024-character tool-description budget (it was 1182).This PR is stacked on #1094 (Cloudflare managed mode), which is stacked on #1093.
User-facing change
run_workflowincludes the API's error body: redacted, JSON compacted, cut at 2 KB with….outbound.errorBodies: 'include' | 'status-only'(default'include').callTool(actionId, input, { throwOnError: false })returns{ ok, status, data, error? }.isError: trueresult with the samestructuredContent.run-workflowexits 1 when the workflow fails.@Toolaccepts aCallToolResultreturn type on a tool with anoutputSchema.action "refundInvoice" failed with status 422→action "refundInvoice" failed with status 422: {"error":"refund_too_large","message":"Refund 500 exceeds the paid amount 120.","maxRefund":120}.callTool(id, input, { throwOnError: false })ignored its third argument and threw → it returns{ ok: false, status: 422, data: {…} }.run_workflowresult had noisErrorand the CLI exited 0 → it hasisError: trueand the CLI exits 1.@Tool({ outputSchema })withexecute(): Promise<Out | CallToolResult>failed to compile → it compiles.run_workflowtool, options, security);create-tool/references/error-handling.md: returning aCallToolResultwithisErrorand structured content.frontmcp-deployment/references/build-for-cli.md:run-workflowexit codes and a troubleshooting row.frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md: error bodies,throwOnError,isError, and two common mistakes.Tests
Added:
action-outcome.spec.ts, 15 tests:'status-only';Bearervalue and a JWT;error-bodies.integration.spec.ts, 11 tests, with a real server and the real@enclave-vminterpreter:'status-only', thrown and returned;throwOnError: falseon success, on an HTTP failure (a script that branches and retries), and on failures with no answer (input validation, authority);isErroron a failed workflow and not on a successful one.run-workflow.tool.spec.ts: 10 new tests and 8 updated:'status-only'andthrowOnErrorpaths.index.spec.ts:outbound.errorBodiesdefaults to'include', accepts'status-only'and rejects anything else.tool-type-safety.spec.ts: a tool with anoutputSchemareturningOut | CallToolResulttypechecks.demo-e2e-skilled-openapi(execution.e2e.spec.ts), over MCP with the production settings:throwOnError: falsescript branches on a 404 and returns a fallback;isError: truewith the same structured content, and a successful one doesn't;demo-e2e-cli-exec(cli-skilled-openapi.e2e.spec.ts):run-workflowexits 0 for a workflow that succeeds and 1 for one that fails, withisErrorand the error in--output json.Run, after merging #1094 and rebuilding
sdk,adapters,plugin-skilled-openapi,testingandcli:ssrf-guard.spec.tsfailed once in four full runs under load: its real DNS lookup of a.invalidhost is slow. It passes alone and on the other runs, and this PR doesn't change it.nx test demo-e2e-skilled-openapi: 5 suites, 52 tests passed.demo-e2e-cli-exec:cli-skilled-openapi.e2e.spec.ts: 11 passed;cli-errors.e2e.spec.ts: 10 passed;cli-login-authorities.e2e.spec.ts: 4 skipped, as always on macOS.libs/skills: 3 suites, 249 passed.nx run <project>:typecheckpassed forplugin-skilled-openapi,sdk,demo-e2e-skilled-openapianddemo-e2e-cli-exec(and itstypecheck-e2e).tsc --noEmiton the plugin's and the SDK'stsconfig.lib.jsonpassed.prettier --checkandeslint(0 errors) passed on the changed files.fix-unused-imports.mjschanged nothing.