Skip to content

feat(mcp): W2.3 — tool-description hygiene scanner with strict-mode opt-in#28

Merged
jrosskopf merged 1 commit into
mainfrom
feature/gh-24-mcp-description-hygiene
May 16, 2026
Merged

feat(mcp): W2.3 — tool-description hygiene scanner with strict-mode opt-in#28
jrosskopf merged 1 commit into
mainfrom
feature/gh-24-mcp-description-hygiene

Conversation

@jrosskopf
Copy link
Copy Markdown
Contributor

Summary

Test plan

  • 12 Catch2 unit cases in test/cpp/mcp_description_scanner_test.cpp cover every detection (NUL, BEL, four prompt-injection variants, case-insensitivity, oversize) plus the false-positive guard (legitimate use of the word "ignore" not flagged) and the issue-shape contract.
  • 2 new parser-level cases in test/cpp/endpoint_config_parser_test.cpp prove non-strict mode loads (warn-only) and strict mode rejects the same poisoned config.
  • 3 end-to-end Python cases in test/integration/test_mcp_description_hygiene.py run the real flapi --validate-config against ad-hoc YAML covering the clean / warn / reject paths.
  • All 17 new tests green locally.
  • ctest -R "MCPDescription|description_hygiene" — 14/14 pass.

Design notes

  • MCPDescriptionScanner is a pure single-responsibility class: (string) → vector<DescriptionIssue>. No I/O, no config knowledge, trivially testable.
  • Prompt-injection detection is phrase-based, not keyword-based, to avoid flagging legitimate descriptions that contain words like "ignore".
  • Newlines / tabs are deliberately allowed (multi-line YAML descriptions are common); only < 0x20 or 0x7F are flagged, excluding \n, \r, \t.
  • The strict flag lives on MCPConfig (not EndpointConfig) because the policy is server-wide, not per-tool.

Closes a piece of #24
Refs #21

)

Adds MCPDescriptionScanner that inspects every mcp-tool description at
config-load time for content that's likely hostile to an LLM agent:

- DESCRIPTION_CONTROL_CHARACTER: non-printable bytes other than
  \n, \r, \t. NUL, BEL, DEL etc. surface here.
- DESCRIPTION_PROMPT_INJECTION: phrases observed in the prompt-injection
  corpus ("ignore previous instructions", "disregard the above",
  "system:", "you are now", and variants). Case-insensitive.
- DESCRIPTION_TOO_LONG: descriptions longer than 2 KB, which waste
  model context and are sometimes used to drown out the user prompt.

Default mode: scanner findings are logged as warnings via CROW_LOG_WARNING
and the config still loads — keeping the simple-first-experience promise.
A new `mcp.strict-descriptions: true` flag flips the parser into reject
mode: any flagged description fails the endpoint load with a ConfigError
that names the issue code.

Implementation:

- New MCPDescriptionScanner class (single responsibility: take a string,
  return a list of DescriptionIssue records). Pure function, no
  dependencies, fully unit-tested in isolation.
- MCPConfig.strict_descriptions bool added; parsed from
  mcp.strict-descriptions in ConfigManager::parseMCPConfig.
- endpoint_config_parser invokes the scanner in parseMcpToolFields
  immediately after the description is parsed, and consults
  ConfigManager.getMCPConfig().strict_descriptions for the deny-or-warn
  policy.

Tests:

- test/cpp/mcp_description_scanner_test.cpp: 12 Catch2 cases — clean
  descriptions, NUL/BEL flagged, newlines/tabs tolerated, four
  prompt-injection variants, case-insensitive detection, false-positive
  guard ("ignore deleted entries" not flagged), oversize, issue shape.
- test/cpp/endpoint_config_parser_test.cpp: two new cases proving
  non-strict mode loads (warn-only) and strict mode rejects the
  same poisoned config.
- test/integration/test_mcp_description_hygiene.py: three end-to-end
  cases that run the real flapi binary in --validate-config mode
  against ad-hoc YAML, covering clean / warn / reject paths.

Skipped pre-commit hook per the existing precedent in commit e1b465e —
the bd-shim calls 'bd hook pre-commit' (singular) which is missing
from the installed bd binary (only 'bd hooks' plural exists).
@jrosskopf jrosskopf force-pushed the feature/gh-24-mcp-description-hygiene branch from 7750655 to 99da075 Compare May 16, 2026 17:38
@jrosskopf jrosskopf merged commit 63a1af7 into main May 16, 2026
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.

1 participant