feat(mcp): filter SafeOutputs tools based on front matter config#156
feat(mcp): filter SafeOutputs tools based on front matter config#156jamesadevine merged 11 commits intomainfrom
Conversation
Only expose safe output tools that are configured in the front matter's safe-outputs section. This reduces agent confusion by hiding tools that aren't configured for the current pipeline. Implementation: - Add --enabled-tools repeatable CLI arg to mcp and mcp-http commands - SafeOutputs::new() accepts optional enabled tools list and uses ToolRouter::remove_route() to remove unconfigured tools at startup - Compiler derives tool list from safe-outputs keys and emits --enabled-tools args in the pipeline template - Always-on diagnostic tools (noop, missing-data, missing-tool, report-incomplete) are never filtered out Backward compatible: if --enabled-tools is not passed (empty safe-outputs or omitted), all tools remain available. Note: MCPG has a tools field in its config schema but does not enforce it at runtime. This change filters at the SafeOutputs server level instead, making it self-contained. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good approach overall — the design is clean and the test coverage is solid. One genuine security concern to address, plus two minor notes. Findings🔒 Security Concerns
|
- Validate tool names against safe regex (ASCII alphanumeric + hyphens) to prevent shell injection from malicious YAML keys - Fix dangling backslash in base.yml when enabled_tools_args is empty - Replace fragile exact-count assertion in test_tool_filtering_multiple_tools with explicit presence/absence checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Needs a fix — the template has a shell-breaking separator bug that will cause the Findings🐛 Bugs / Logic Issues
|
- Add trailing space to generate_enabled_tools_args output when non-empty, preventing the last --enabled-tools value from concatenating with the next positional argument in the shell command - Move ALWAYS_ON_TOOLS constant from mcp.rs to tools/mod.rs to avoid compile→mcp coupling (common.rs now imports from tools directly) - Reduce list_all() calls in tool filtering to a single collect pass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Mostly solid — the core design, security model, and backward compatibility are all well-handled. One correctness concern worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
- Add ALL_KNOWN_SAFE_OUTPUTS constant in tools/mod.rs enumerating every
valid safe-output key (MCP tools + always-on diagnostics + memory)
- Emit compile-time warning when a safe-outputs key doesn't match any
known tool, catching typos like 'crate-pull-request' early
- Use HashSet for O(1) deduplication when merging always-on tools
- Rename misleading test to test_generate_enabled_tools_args_warns_on_unknown_tool
and exercise the typo path (crate-pull-request)
- Document {{ enabled_tools_args }} template marker in AGENTS.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Approach is solid and the implementation is clean — one correctness bug with misleading behaviour, a maintenance concern, and a missing integration test. Findings🐛 Bugs / Logic Issues
|
Previously, unrecognized tool names (e.g. typos like "crate-pull-request") were warned about but still appended to --enabled-tools. This caused the real tool (create-pull-request) to be silently filtered out at runtime because it was absent from the enabled list. Now unrecognized names are skipped entirely, making the warning and behavior consistent. The warning message is also updated to be clearer. Additional improvements: - Add test asserting ALL_KNOWN_SAFE_OUTPUTS covers every router-registered tool, preventing the list from drifting when new tools are added - Add integration test verifying --enabled-tools flags appear in compiled pipeline YAML (end-to-end: standalone.rs + generate_enabled_tools_args + template substitution) - Document is_safe_tool_name newline-safety requirement - Add explanatory comment in base.yml template for the inline substitution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Mostly solid implementation with one real bug and one subtle footgun worth addressing before merge. Findings🐛 Bugs / Logic Issues
|
Address PR review feedback: - Skip non-MCP safe-output keys (e.g. `memory`) from --enabled-tools generation. These keys have no MCP route, so including them would cause real MCP tools to be filtered out at runtime. - Add prominent warning when all user-specified safe-output keys are invalid/unrecognized/non-MCP, since the agent would be restricted to diagnostic tools only. - Remove unreachable `args.is_empty()` branch — ALWAYS_ON_TOOLS guarantees the args string is never empty when safe-outputs is configured. - Add tests for memory key skipping and memory-only configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Good approach with one meaningful bug — the Findings🐛 Bugs / Logic Issues
When This is a silent behavioral regression for any pipeline that has The test The fix is a one-liner after the loop: if user_tool_count == 0 {
// No real MCP tools configured; keep all tools available (backward compat)
return String::new();
}The warning already runs before this return, so users with only
|
When safe-outputs contains only non-MCP keys (e.g. just `memory`) or only unrecognized names (typos), return empty args so all tools remain available — matching backward-compatible behavior. Previously this path would emit only ALWAYS_ON_TOOLS in --enabled-tools, silently restricting the agent to 4 diagnostic tools. Also: - Add debug! log in server-side filtering for enabled-tools entries that have no matching route, aiding troubleshooting - Add template comment documenting positional arg ordering requirement Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — solid design and well-tested. A few minor observations worth considering before merge. Findings
|
Replace hand-maintained string arrays with compile-time type-derived lists using two new macros: - tool_names![Type1, Type2, ...] → extracts ToolResult::NAME from each type into a &[&str] array - all_safe_output_names![types...; "extra"] → combines type names with non-MCP string keys Changes: - Add ToolResult::REQUIRES_WRITE associated constant (default false) - Extend tool_result! macro with `write = true` parameter (4 arms) - Annotate all 17 write-requiring tools with write = true - CreatePrResult (manual impl) also gets REQUIRES_WRITE = true - Move WRITE_REQUIRING_SAFE_OUTPUTS from common.rs to tools/mod.rs - Derive ALWAYS_ON_TOOLS, WRITE_REQUIRING_SAFE_OUTPUTS, and ALL_KNOWN_SAFE_OUTPUTS from type lists — no more string duplication - Add NON_MCP_SAFE_OUTPUT_KEYS as public const in mod.rs - Add 5 subset/consistency tests: - WRITE_REQUIRING ⊆ ALL_KNOWN - ALWAYS_ON ⊆ ALL_KNOWN - NON_MCP ⊆ ALL_KNOWN - REQUIRES_WRITE consistency (true for write tools, false for diag) - ALL_KNOWN count = write + diagnostic + non-MCP Adding a new tool now requires adding its type to the list in mod.rs; the name string is derived automatically from ToolResult::NAME, eliminating the risk of typo drift between lists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rpose - Add note in onees.rs explaining that 1ES target does not support --enabled-tools filtering (uses service connections, not mcp-http) - Clarify HashSet comment in generate_enabled_tools_args — it deduplicates across user keys and ALWAYS_ON_TOOLS, not within HashMap keys Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-designed feature with thorough tests. One doc-comment bug worth fixing before merge. Findings🐛 Bugs / Logic Issues
|
- Fix doc comment: example showed two semicolons but macro only accepts one. Clarify that all types go before the semicolon, string keys after. - Rename user_tool_count → effective_mcp_tool_count for clarity — it counts recognized, valid, non-MCP keys, not all user-specified keys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — solid approach, well-tested, safe. Two minor issues worth addressing before merge. Findings
|
… checks - Promote debug! to warn! for --enabled-tools entries with no matching route, matching the compile-time warning severity. A typo like "crate-pull-request" would otherwise be invisible in production logs. - Add explicit disjointness assertions in test_all_known_completeness so overlapping lists produce a clear "tool X in both lists" message rather than a confusing count mismatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 Rust PR ReviewSummary: Looks good — well-structured feature with strong tests; one minor logic quirk worth noting for future maintainers. Findings✅ What Looks Good
|
Summary
Only expose safe output tools that are configured in the front matter's
safe-outputs:section. This reduces agent confusion by hiding tools that aren't configured for the current pipeline.Problem
All 17+ safe output tools are always exposed to the agent via MCPG, regardless of what's configured in
safe-outputs:front matter. MCPG has atoolsfield in its config schema but does not enforce it at runtime. This means agents see tools they aren't configured to use, wasting context window and potentially causing confusion.Approach
Filter at the SafeOutputs server level using
ToolRouter::remove_route()— self-contained, no MCPG changes needed.Changes
src/main.rs— Add--enabled-toolsrepeatable CLI arg tomcpandmcp-httpcommandssrc/mcp.rs—SafeOutputs::new()accepts optional enabled tools list; removes unconfigured tools from the router at startupsrc/compile/common.rs—generate_enabled_tools_args()derives tool list fromsafe-outputs:keys + always-on diagnosticssrc/compile/standalone.rs— Wires the generated args into the pipeline templatetemplates/base.yml—{{ enabled_tools_args }}placeholder inmcp-httpinvocationAlways-on tools (never filtered)
noop,missing-data,missing-tool,report-incompleteBackward compatibility
If
--enabled-toolsis not passed (empty/omittedsafe-outputs:), all tools remain available.Testing