Skip to content

Zero-argument custom safe-output tools show help instead of calling MCP #49301

Description

@tobio

Problem statement

The MCP CLI bridge currently prevents valid zero-argument custom safe-output tools from being called. Every safeoutputs invocation whose parsed arguments are {} is treated as a schema-discovery probe, so the bridge prints per-tool help and returns before issuing MCP tools/call.

This is a runtime regression: the compiler intentionally supports custom safe-output jobs with no inputs and generates an empty object schema for them.

{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

User impact and use case

Downstream repository: https://github.com/elastic/terraform-provider-elasticstack

The affected workflow uses a custom safe-output tool named dispatch_code_factory. Calling the tool records a safe-output item; a later dispatch_code_factory GitHub Actions job checks for that output type before dispatching code-factory runs for issues created by the agent.

Because the bridge exits after showing help, no item is recorded. The later job's if: condition evaluates false and silently skips the intended dispatches.

Evidence:

The downstream repository is adding a required dummy boolean input as a temporary mitigation. That avoids {} but does not resolve the general runtime contract violation.

Reproduction

Given a generated tool named dispatch_code_factory with the empty schema above, both supported zero-argument forms fail:

$ safeoutputs dispatch_code_factory
[warning] [safeoutputs] No arguments provided for 'dispatch_code_factory'; showing command help instead of calling the tool

$ printf '{}' | safeoutputs dispatch_code_factory .
[warning] [safeoutputs] No arguments provided for 'dispatch_code_factory'; showing command help instead of calling the tool

Expected behavior

Both invocations should proceed through MCP initialization and issue:

{
  "method": "tools/call",
  "params": {
    "name": "dispatch_code_factory",
    "arguments": {}
  }
}

The safe-output item should then be recorded and the downstream custom job should be eligible to run.

Actual behavior

The bridge invokes showToolHelp, returns with a successful exit, and never sends tools/call.

Agent analysis and execution trace

The failure is in actions/setup/js/mcp_cli_bridge.cjs, not in custom-job schema generation or the MCP server:

  1. main() loads the cached tool definitions.
  2. It resolves matchedTool, including matchedTool.inputSchema.
  3. parseToolArgs correctly produces {} for both a bare invocation and piped {} with the . sentinel.
  4. shouldShowToolHelpForEmptyArgs(serverName, toolArgs) currently ignores matchedTool and returns true for every safeoutputs call with {}.
  5. main() displays help and returns before mcpInitialize, mcpNotifyInitialized, and mcpToolsCall.

Current guard:

function shouldShowToolHelpForEmptyArgs(serverName, toolArgs) {
  return serverName === SAFEOUTPUTS_SERVER_NAME && Object.keys(toolArgs).length === 0;
}

The server path in actions/setup/js/mcp_server_core.cjs is already schema-aware: empty-argument probe rejection is only relevant when the tool declares required fields. It accepts {} for a schema with no required inputs.

The compiler path in pkg/workflow/safe_outputs_config_generation.go also behaves correctly. generateCustomJobToolDefinition always emits an object schema, populates properties from configured inputs, and only adds required when at least one input is required. A custom job with omitted inputs therefore intentionally produces properties: {} and no required field.

Regression source and affected versions

The behavior was introduced by #43566 and merge commit 1ac3557.

The original change was intended to stop agents repeatedly probing write-once safe-output tools with {}, receiving -32602, and retrying until timeout. During review, schema-aware detection was simplified based on the assertion that safe-output tools always require arguments: #43566 (comment)

That assertion does not hold for custom safe-output jobs.

Version evidence:

Version Status
setup v0.81.6 / AWF v0.27.11 Known good
v0.82.3 First affected release containing the merge commit
v0.83.4 Affected
v0.84.0 Affected

The known-good lock file already generated an empty schema, confirming this is bridge behavior drift rather than a downstream schema change.

Similar-issue research

Before filing, searches were performed for the regression PR number, shouldShowToolHelpForEmptyArgs, zero-argument safeoutputs, empty-argument safeoutputs, and dispatch_code_factory. No existing gh-aw issue or fix PR describing this exact regression was found. The downstream issue linked above is the only identified report.

Implementation plan

Please have a core-team coding agent implement the following steps in order. Each step identifies the files, behavior, tests, and validation needed for a complete fix.

1. Restore schema-aware probe detection

Update shouldShowToolHelpForEmptyArgs in actions/setup/js/mcp_cli_bridge.cjs to receive the already-resolved matchedTool.

For an empty parsed argument object:

  • return false for servers other than safeoutputs;
  • inspect matchedTool.inputSchema.required;
  • show help only when required is a non-empty array;
  • allow zero-input and optional-only tools to proceed with {}.

Using required rather than the number of properties follows JSON Schema semantics: an optional-only tool can also be validly invoked with {}. Missing or malformed cached schema should not be misclassified as a required-input probe; the MCP server remains authoritative and can return the appropriate protocol error.

Pass matchedTool at the call site in main() and update the JSDoc. Do not remove the existing -32602 non-retry hint added by #43566; the fix should preserve that protection for invalid calls.

2. Add focused bridge tests

Update actions/setup/js/mcp_cli_bridge.test.cjs:

  • empty args + explicit empty object schema returns false from the help guard;
  • empty args + optional-only schema returns false;
  • empty args + non-empty required returns true;
  • non-empty calls and non-safeoutputs servers remain unchanged;
  • printf '{}' | safeoutputs dispatch_code_factory . reaches MCP tools/call with arguments: {};
  • bare safeoutputs dispatch_code_factory reaches the same call;
  • a normal required-input tool still prints help and does not call MCP.

Use a local in-process HTTP server for the bridge-level tests so the assertions cover the actual initialize → initialized notification → tools/call route without a live external API.

3. Preserve the compiler/runtime contract

Retain the existing no inputs unit case in pkg/workflow/safe_outputs_config_generation_test.go.

Extend pkg/workflow/safe_outputs_tools_meta_integration_test.go with a custom safe-output job whose inputs are omitted. After compilation, parse GH_AW_TOOLS_META_JSON and assert its dynamic_tools entry contains:

  • normalized tool name;
  • type: object;
  • empty properties;
  • no required key;
  • additionalProperties: false.

Note: explicit inputs: {} is currently rejected by the workflow schema's minProperties: 1; omitted inputs are the supported equivalent and generate the empty MCP schema at issue here.

4. Release metadata

Add a patch changeset describing restoration of valid zero-argument custom safe-output calls. This is a backward-compatible bug fix restoring the compiler/runtime contract, not a CLI breaking change.

No user documentation change should be required: the public configuration is already valid and the defect is in runtime routing. If maintainers want to clarify zero-input custom jobs explicitly, that can be handled as a separate documentation improvement.

5. Validation

The implementing core-team agent should work in the supported Dev Container or Codespace and run the repository-prescribed checks, including make agent-finish, before completing its PR.

Focused checks:

cd actions/setup/js
npm run typecheck
vitest run mcp_cli_bridge.test.cjs --no-file-parallelism

cd ../../..
go test ./pkg/workflow -run TestGenerateCustomJobToolDefinition
go test -tags=integration ./pkg/workflow -run TestToolsMetaJSONCompiledWorkflowEmbedsMeta

Reference implementation and local verification

A reference implementation was built locally at tobio/gh-aw@c159b95877. The issue is intentionally self-contained so a core-team agent can implement or adapt it according to the repository's contribution model.

Checks already completed against that reference implementation:

  • make build
  • make lint-cjs
  • npm run typecheck
  • focused bridge suite: 108 tests passed
  • go test ./pkg/workflow -run TestGenerateCustomJobToolDefinition: 11 tests passed
  • go test -tags=integration ./pkg/workflow -run TestToolsMetaJSONCompiledWorkflowEmbedsMeta: passed
  • make agent-report-progress: passed after installing the repository-pinned golangci-lint version

This local verification supplements, but does not replace, the required supported-environment validation by the implementing core-team agent.

Acceptance criteria

  • Empty calls to safe-output tools with no required fields reach MCP tools/call with {}.
  • Both bare invocation and piped {} invocation work for a zero-input custom tool.
  • Optional-only tools are not incorrectly blocked.
  • Empty calls to tools with required fields still show help and do not call MCP.
  • Non-safeoutputs behavior remains unchanged.
  • The existing -32602 non-retry hint remains intact.
  • Compiler integration coverage protects the empty-schema contract.
  • A patch changeset is included.
  • Focused tests and make agent-finish pass in the supported development environment.

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions