Skip to content

Use actual mounted MCP CLI wrappers so AI Moderator safe outputs remain reliable#39243

Closed
Copilot wants to merge 3 commits into
mainfrom
copilot/debug-ai-moderator-workflow-failure
Closed

Use actual mounted MCP CLI wrappers so AI Moderator safe outputs remain reliable#39243
Copilot wants to merge 3 commits into
mainfrom
copilot/debug-ai-moderator-workflow-failure

Conversation

Copilot AI commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

AI Moderator could complete without safe outputs when MCP CLI wrappers were advertised in the prompt but not actually mounted at runtime, causing a “no safe outputs generated” failure path in an otherwise successful run.

  • CLI generator/runtime alignment fix

    • Moved the fix from prompt wording to the CLI generator path.
    • Added a new mounted-servers-list output in mount_mcp_as_cli.cjs derived from successfully mounted wrappers.
    • Updated workflow prompt wiring to use steps.mount-mcp-clis.outputs.mounted-servers-list for GH_AW_MCP_CLI_SERVERS_LIST, so only real, mounted wrappers are advertised to the model.
  • Prompt/test updates

    • Removed the prior fallback sentence from mcp_cli_tools_prompt.md (no longer needed as primary mitigation).
    • Updated Go prompt-section test expectations to validate the runtime output binding.
    • Added JS test coverage for mounted-server prompt list formatting.

This keeps prompt guidance consistent with runtime capability and prevents false missing-wrapper paths from steering successful runs into missing-safe-output handling.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Debug AI Moderator workflow failure for safe outputs Clarify MCP CLI fallback so AI Moderator still emits safe outputs Jun 14, 2026
Copilot AI requested a review from pelikhan June 14, 2026 16:23
@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot fix the ai moderator or the cli generator instead .

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix the ai moderator or the cli generator instead .

Implemented in 67ce9d1. I moved the fix to the CLI generator path: the MCP CLI prompt list now comes from steps.mount-mcp-clis.outputs.mounted-servers-list, so only actually mounted wrappers are advertised. I also reverted the prior prompt-only fallback sentence and updated JS/Go tests accordingly.

Copilot AI changed the title Clarify MCP CLI fallback so AI Moderator still emits safe outputs Use actual mounted MCP CLI wrappers so AI Moderator safe outputs remain reliable Jun 14, 2026
@github-actions github-actions Bot mentioned this pull request Jun 14, 2026
@pelikhan pelikhan marked this pull request as ready for review June 14, 2026 21:44
Copilot AI review requested due to automatic review settings June 14, 2026 21:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to keep the MCP CLI tools prompt consistent with runtime reality by advertising only MCP CLI wrappers that were actually mounted, preventing “no safe outputs generated” failures caused by the model being told wrappers exist when they don’t.

Changes:

  • Adds a new mounted-servers-list step output from mount_mcp_as_cli.cjs for prompt-ready bullet formatting.
  • Updates MCP CLI prompt section wiring to use the mount step output instead of a compile-time generated list.
  • Extends unit tests (Go + JS) to validate the new output binding and formatting.
Show a summary per file
File Description
pkg/workflow/mcp_cli_mount.go Switches prompt env var binding to reference the mount step output.
pkg/workflow/mcp_cli_mount_test.go Updates expectations to assert the new env var wiring.
actions/setup/js/mount_mcp_as_cli.cjs Adds prompt-list formatting helper and exports it; emits new mounted-servers-list output.
actions/setup/js/mount_mcp_as_cli.test.cjs Adds test coverage for prompt list formatting.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment on lines 312 to 316
Content: mcpCLIToolsPromptFile,
IsFile: true,
EnvVars: map[string]string{
"GH_AW_MCP_CLI_SERVERS_LIST": serversList,
"GH_AW_MCP_CLI_SERVERS_LIST": "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}",
},
Comment on lines +320 to +322
function formatMountedServersPromptList(mountedServers) {
return mountedServers.map(name => `- \`${name}\` — run \`${name} --help\` to see available tools`).join("\n");
}
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #39243 does not have the 'implementation' label and has only 7 new lines in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnose and /tdd — approving with a few non-blocking observations. The core correctness fix is sound and the data-flow shift from compile-time to runtime is the right approach.

📋 Key Themes & Highlights

Key Themes

  • One edge case to consider: the nil-guard in buildMCPCLIPromptSection uses configured servers (compile-time), so an all-servers-fail-to-mount scenario at runtime will still generate the prompt section — but with an empty bullet list. Not a blocker, but worth a follow-up.
  • Magic step ID: mount-mcp-clis is embedded as a string literal in the Go-generated expression — a constant would make renaming safe.
  • PR description vs. diff: The body mentions removing a fallback sentence from mcp_cli_tools_prompt.md, but that file is not in the diff. Confirm whether this was done in an earlier commit on the branch or is still pending.

Positive Highlights

  • formatMountedServersPromptList is a clean, pure function — easy to test and reason about
  • ✅ Empty-array edge case is explicitly covered in the new test
  • ✅ Go simplification (removing the compile-time list-building loop) is a welcome side effect
  • ✅ The fix is at the right layer — runtime output, not prompt wording
  • ✅ Both JS and Go test layers updated consistently

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 232 AIC · ⌖ 14.2 AIC · ⊞ 27.9K

func buildMCPCLIPromptSection(data *WorkflowData) *PromptSection {
servers := getMCPCLIServerNames(data)
if len(servers) == 0 {
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The nil-guard uses getMCPCLIServerNames(data) — configured servers — not the runtime mount result. In the partial-failure scenario where all configured servers fail to mount, this prompt section is still generated; at runtime ${{ steps.mount-mcp-clis.outputs.mounted-servers-list }} resolves to "", so the model receives the "CLI servers available on PATH" preamble with an empty bullet list.

💡 Suggested mitigation

This is an edge case, but could produce confusing model behaviour. Options:

  1. Document it in the comment — note that a partial-mount failure may produce an empty server list in the rendered prompt.
  2. Template-level guard — if mcp_cli_tools_prompt.md can wrap the bullet list in a conditional block ({% if GH_AW_MCP_CLI_SERVERS_LIST %}...{% endif %}), the empty-list case degrades cleanly.
  3. Runtime guard — have the step skip setting the mounted-servers-list output (or emit a sentinel value) when the list is empty, so downstream consumers can detect the empty case.

EnvVars: map[string]string{
"GH_AW_MCP_CLI_SERVERS_LIST": serversList,
"GH_AW_MCP_CLI_SERVERS_LIST": "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The step ID mount-mcp-clis is a magic string embedded in this Go-generated runtime expression. If the step is renamed in the workflow definition, the binding breaks silently — no compile-time check catches the mismatch.

💡 Suggested improvement

Define a Go constant that holds the step ID and reference it when building the expression:

const mcpMountStepID = "mount-mcp-clis"

// ...
"GH_AW_MCP_CLI_SERVERS_LIST": fmt.Sprintf("${{ steps.%s.outputs.mounted-servers-list }}", mcpMountStepID),

This also makes it easier to verify consistency with the step definition elsewhere in the Go layer.

});
});

it("formats mounted servers for prompt rendering", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The single it block combines a happy-path assertion and the empty-array regression case. Splitting them into two named it blocks means failure messages directly identify the broken scenario, and the regression intent is self-documenting.

💡 Suggested split
it("formats multiple mounted servers into prompt bullet lines", () => {
  expect(formatMountedServersPromptList(["safeoutputs", "playwright"]))
    .toBe("- `safeoutputs` — run `safeoutputs --help` to see available tools\n- `playwright` — run `playwright --help` to see available tools");
});

it("returns empty string when no servers are mounted (regression: partial/full mount failure must not advertise absent wrappers)", () => {
  expect(formatMountedServersPromptList([])).toBe("");
});

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 2 test(s): 2 design, 0 implementation, 0 guideline violations.

📊 Metrics & Test Classification (2 tests analyzed)
Metric Value
New/modified tests analyzed 2
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (100%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
it("formats mounted servers for prompt rendering") actions/setup/js/mount_mcp_as_cli.test.cjs:40 ✅ Design None — covers normal + empty-input edge case
TestBuildMCPCLIPromptSection_PromptFileUsesNonHeadingLabels (+1 assertion) pkg/workflow/mcp_cli_mount_test.go:226 ✅ Design New assert.Equal on EnvVars key lacks a message arg (minor)

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration) ✅
  • 🟨 JavaScript (*.test.cjs): 1 test (vitest)
⚠️ Flagged Tests — Requires Review (1 minor issue)

⚠️ TestBuildMCPCLIPromptSection_PromptFileUsesNonHeadingLabels (pkg/workflow/mcp_cli_mount_test.go:229)

Classification: Design test ✅
Issue: The newly added assertion assert.Equal(t, "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}", section.EnvVars["GH_AW_MCP_CLI_SERVERS_LIST"]) is missing a descriptive message argument. Without it, a failure only prints the raw values with no human-readable context.
Suggested improvement:

assert.Equal(t, "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}", section.EnvVars["GH_AW_MCP_CLI_SERVERS_LIST"],
    "GH_AW_MCP_CLI_SERVERS_LIST must reference the mount-mcp-clis step output")

Note: this is a minor style guideline issue and does not affect the verdict.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). Both tests verify observable behavioral contracts:

  • The JS test asserts the exact formatted string output of formatMountedServersPromptList() for a non-empty list and the empty edge case.
  • The Go test closes a genuine coverage gap by asserting that buildMCPCLIPromptSection wires the correct GitHub Actions step-output expression into EnvVars["GH_AW_MCP_CLI_SERVERS_LIST"] — a contract that would catch regressions if the step name or output key changed.
📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · 345.9 AIC · ⌖ 50.5 AIC · ⊞ 26.8K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). Both the new JS test (formatMountedServersPromptList with normal + empty-input edge case) and the Go assertion extension (verifying EnvVars["GH_AW_MCP_CLI_SERVERS_LIST"] contract) are design tests that enforce real behavioral contracts.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking concerns; the fix does not achieve its stated goal in the common case.\n\n

\n### Blocking themes\n\nActivation-job expression resolution (critical, already flagged inline)GH_AW_MCP_CLI_SERVERS_LIST is wired to ${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}, but that expression is evaluated inside the activation job, which has no mount-mcp-clis step. The env var resolves to an empty string there. The prompt section is still emitted (guarded only by the config-time server count, not the runtime count), so the model receives a prompt advertising CLI tools with a blank server list.\n\nFragile literal-string test — the new assertion in mcp_cli_mount_test.go validates only that the expression string is spelled as written today. A step or output rename causes a silent runtime regression that tests do not catch (see inline comment).\n\n

🔎 Code quality review by PR Code Quality Reviewer · 227.5 AIC · ⌖ 13.3 AIC · ⊞ 16.7K

assert.NotRegexp(t, `(?m)^\s*(>\s*)?###\s+`, prompt, "prompt must not contain H3 Markdown headings")
assert.Contains(t, prompt, "Use `<server> --help` for tool names, parameters, and examples before calling any command.")
assert.Equal(t, "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}", section.EnvVars["GH_AW_MCP_CLI_SERVERS_LIST"])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile magic-string assertion will not catch a step/output rename: this only validates the expression is spelled correctly today, not that the env var will be populated at runtime.

💡 Details

The new assertion:

assert.Equal(t, "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}", section.EnvVars["GH_AW_MCP_CLI_SERVERS_LIST"])

has no compile-time or test-time link to the step ID mount-mcp-clis or the output key mounted-servers-list generated elsewhere in the compiler. If either name drifts (e.g., step is renamed to mount-mcp-cli-tools), the generated YAML silently produces an empty server list and this test continues to pass with exit 0.

Suggested fix: extract the expression to a named constant in the package (e.g., const mcpCLIMountedServersListExpr = "${{ steps.mount-mcp-clis.outputs.mounted-servers-list }}"), use it in both the production code and the test, and add a TestMain or init-time assertion that the step ID constant matches what the compiler actually emits for the mount step. That way a rename breaks in exactly one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants