Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@ For each language recon cleared, do the AST work and produce a `RepoInventory`:
`#[...]` is parsed as a `comment` node — so the attribute is read from the
comment text immediately preceding the method via regex. Emits
`ToolDef{Kind: mcp_tool, Language: php}`, so deriveSDKsDetected stamps SDKMCP
and the mcp/ pack's `language: php` rules (MCP-019/020) audit them. Multi-line
and the mcp/ pack's `language: php` rules (MCP-019/020/029) audit them. Multi-line
`#[...]` attributes, `#[McpResource]` / `#[McpPrompt]`, and PHP body-fact
predicates are v1 gaps.
- **DiscoverRustMCPTools** (`rust_mcp.go`) — Rust MCP tools parsed with
Expand Down
15 changes: 15 additions & 0 deletions internal/rules/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,21 @@ def run_cmd(name: str) -> str:
kind: models.KindMCPTool, lang: models.LanguagePHP, wantFires: false,
src: "<?php\nuse PhpMcp\\Server\\Attributes\\McpTool;\nclass T {\n #[McpTool(name: 'summarize_invoice', description: 'Summarize')]\n public function summarizeInvoice(string $input): string { return $input; }\n}\n",
},
{
name: "MCP-029 fires on PHP untyped params", ruleID: "MCP-029",
kind: models.KindMCPTool, lang: models.LanguagePHP, wantFires: true,
src: "<?php\nuse PhpMcp\\Server\\Attributes\\McpTool;\nclass T {\n #[McpTool(name: 'search_docs', description: 'Search')]\n public function searchDocs($query, $limit) { return $query; }\n}\n",
},
{
name: "MCP-029 silent with PHP type hints", ruleID: "MCP-029",
kind: models.KindMCPTool, lang: models.LanguagePHP, wantFires: false,
src: "<?php\nuse PhpMcp\\Server\\Attributes\\McpTool;\nclass T {\n #[McpTool(name: 'search_docs', description: 'Search')]\n public function searchDocs(string $query, int $limit): string { return $query; }\n}\n",
},
{
name: "MCP-029 silent on zero-param PHP tool", ruleID: "MCP-029",
kind: models.KindMCPTool, lang: models.LanguagePHP, wantFires: false,
src: "<?php\nuse PhpMcp\\Server\\Attributes\\McpTool;\nclass T {\n #[McpTool(name: 'ping', description: 'Health check')]\n public function ping(): string { return 'ok'; }\n}\n",
},
{
name: "MCP-021 fires on Rust tool with no description", ruleID: "MCP-021",
kind: models.KindMCPTool, lang: models.LanguageRust, wantFires: true,
Expand Down
24 changes: 24 additions & 0 deletions testdata/rules-fixture/mcp/tool_definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,27 @@ rules:
fix: >
Rename the method (or set the `#[tool]` `name = "..."` argument) to a
verb-object form, e.g. `summarize_invoice`, `fetch_weather`.

- id: MCP-029
title: PHP MCP tool has no type-annotated parameters
severity: medium
confidence: 0.85
language: php
applies_to:
- mcp_tool
scope: tool
match:
has_params: true
has_typed_params: false
explanation: >
MCP derives a PHP tool's advertised input JSON schema from the handler's
parameter type hints. PHP type hints are optional; without them the
published schema is unconstrained, so connecting models send unvalidated
inputs that frequently cause runtime errors inside the server. Go, C#,
and Rust MCP tools cannot express this gap — those languages are
statically typed — which is why this sibling of MCP-002 exists only for
PHP.
fix: >
Add type hints to every parameter (`function search(string $query, int
$limit = 10): array`). Use typed properties or DTO classes for structured
inputs so the published schema constrains them.