feat(mcp): add MCP-029, PHP tool has no type-annotated parameters - #80
feat(mcp): add MCP-029, PHP tool has no type-annotated parameters#80eulestadt wants to merge 2 commits into
Conversation
PHP type hints are optional and discovery already captures HasTypedParams; this is the MCP-002 sibling the rulebook listed as a deliberate fast-follow. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Companion PRs:
|
bradAGI's open PR stack claims MCP-023 through MCP-028. A duplicated rule ID is rejected by the loader at startup, so shipping both would break every scan rather than fail a test. MCP-029 is the first free ID above that stack. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks @eulestadt, this is a model contribution. The rule is a clean PHP sibling of One coordination note: MCP-029 is also claimed by #94, #100, trustabl/trustabl#135, and trustabl/trustabl#172. Whichever PR lands first keeps the ID and the others will need to renumber and rebase; that is on us to sequence, not a defect here. The fixture mirror in trustabl/trustabl#124 matches this YAML byte for byte, so the pair is sound, and we will review the rationale doc in trustabl-rulebook#45 alongside it since every shipped rule needs one before it goes out. |
Renamed this from MCP-023 to MCP-029 before anything else, because #82, #85, #88 and #91 claim 023 through 028. I filed 023 about four minutes earlier, but that's not really the point — Brad's stack occupies the whole run above it, so yielding is one edit for me against five for him. A duplicate rule ID stops the loader at startup rather than failing a test, so two green PRs merging an hour apart would have broken every scan until someone sorted it out. The branch is still named
mcp-023; renaming it would close this PR, so I left it alone.The rule itself: nine of ten packs ship an untyped-parameters rule, and within MCP,
MCP-002covers Python while PHP had nothing. PHP is the only other MCP language where the gap can exist — Go, C# and Rust are statically typed, so the handler signature always constrains the published schema and there's nothing to detect. That's why this is a two-language rule and not a five-language one, and why wideningMCP-002would have been wrong: its fix text prescribes Python annotations.Mechanically, MCP builds the advertised input schema from the handler's parameter type hints. With no hints the schema is unconstrained, so the model sends whatever it infers from the description and the failure lands inside the server as a type error or a quiet coercion, instead of being rejected at the boundary where a schema would catch it.
medium/0.85, matching
MCP-002exactly — high for a heuristic because the signal is structural rather than name-based.I scanned a PHP MCP server with four handlers.
searchDocs($query, $limit)fires.fetchInvoice(string $invoiceId, int $version = 1): arraystays quiet, which covers the hinted-with-a-default shape most likely to trip a naive matcher.ping(): stringstays quiet becausehas_params: truegates it out — without that conjunct every no-arg health check in every PHP server would fire.The fourth handler is the honest part.
HasTypedParamsis set when any parameter carries a type, so one hint silences the whole signature —mixedHints(string $query, $limit)reports nothing despite$limitbeing bare. I checked that with a scan rather than assuming it from the code. It's the reason for 0.85, and tightening it would mean an all-params-typed predicate that changesMCP-002and eight other packs, so it belongs in its own PR. Schemas published outside the signature and#[McpResource]/#[McpPrompt]are out of scope too.has_paramsandhas_typed_paramsboth already ship, so no schema bump. Fixture mirror is trustabl#124 and the rationale is trustabl-rulebook#45; this one needs to merge first, sincerules-synccan't see a fork branch and the engine check stays red until it does.Made with Cursor