The Model Context Protocol (MCP) lets agents call tools exposed by third-party servers. You install a server from npm or PyPI, point your agent at it, and suddenly your LLM can read files, execute shell commands, or query databases. The problem: you have no idea what those tools actually do until runtime, and by then it’s too late.
Canopii CLI is a deterministic scanner that runs static analysis on MCP servers before you connect them. It detects command injection sinks, prompt-injection markers in tool descriptions, over-broad or destructive tools, and committed secrets. It scans the entire official MCP registry continuously and makes results available via CLI or web.
This is not an LLM-based analyzer. It’s a rule engine that parses tool definitions, traces data flow through code, and flags patterns that create security boundaries you can’t enforce at runtime.
Why Static Analysis Matters for Agent Tools
MCP servers expose tools as JSON schemas with handler functions. A typical tool definition includes:
- A name and description (sent to the LLM as part of the prompt)
- Input parameters (typed, but not validated beyond JSON schema)
- A handler function that receives those parameters and returns a result
The LLM decides when to call the tool and what arguments to pass. You have no control over that decision once the tool is registered. If the tool description says “delete files matching a pattern,” the LLM will call it when it thinks that’s appropriate. If the handler passes user input to child_process.exec(), you have remote code execution.
Static analysis catches these issues before the server runs:
- Command injection: Detects
exec(),spawn()with shell enabled, or string interpolation into shell commands - Prompt-injection markers: Flags phrases in tool descriptions that could manipulate the LLM’s reasoning (“ignore previous instructions,” “system override”)
- Over-broad tools: Identifies tools with destructive verbs (delete, remove, drop) or file-system wildcards
- Committed secrets: Scans for API keys, tokens, and credentials in source code or config files
How the Scanner Works
Canopii CLI combines three engines:
- Semgrep for code-safety rules (command injection, dynamic code execution)
- Gitleaks for secret detection
- Custom rules for tool-definition analysis (prompt markers, over-broad tools)
The installer script downloads checksum-verified binaries for Semgrep and Gitleaks. The CLI orchestrates them and merges results into a single score.
Detection Heuristics
Command injection is straightforward: any call to child_process.exec(), child_process.spawn({ shell: true }), or Python’s os.system() with user-controlled input is flagged. The scanner traces data flow from tool parameters to these sinks.
Prompt-injection markers are regex-based. The scanner looks for phrases like:
- “ignore previous instructions”
- “system override”
- “disregard all prior context”
- “new instructions:”
These patterns appear in tool descriptions that could trick the LLM into bypassing safety guardrails. False positives happen when legitimate tools describe security features (“this tool ignores invalid input”). The scanner flags them anyway because the risk is asymmetric.
Over-broad tools are detected by combining:
- Destructive verbs in the tool name or description (delete, remove, drop, truncate)
- File-system wildcards (
*,**) in parameter schemas - Lack of confirmation parameters (no
confirm: trueor similar)
A tool named delete_files with a pattern parameter that accepts **/* gets flagged. A tool named delete_file with a path parameter does not.
Committed secrets use Gitleaks’ entropy-based detection plus known patterns for API keys (OpenAI, Anthropic, AWS, etc.). The scanner runs against the entire repository, not just the MCP server code, because secrets in test files or example configs are still secrets.
Scanning the Registry
The CLI can scan:
- GitHub repositories (
--github owner/repo) - npm packages (
--npm @scope/package) - PyPI packages (
--pypi package-name) - Local directories (
--local .) - Live MCP endpoints (
--remote https://api.example.com/mcp --api-key $KEY)
For registry-wide scanning, Canopii runs a scheduled job that:
- Fetches the official MCP server list from the Anthropic registry
- Clones each repository or downloads each package
- Runs the full scan suite
- Computes a risk score (0-100, F to A+)
- Publishes results to index.canopii.dev
The score is capped by the worst confirmed issue. A server with one critical command-injection sink cannot score above 30, even if it passes every other control. This prevents “good practices” from masking critical flaws.
Example Scan Output
$ npx canopii scan --github modelcontextprotocol/servers
[email protected] (tier 3 · key-gated)
Score 7/100 (F) capped by code.no_command_injection (critical)
Confidence 78% (high) · scoring v2.8.0
Code safety
✗ No command-injection sinks — 2 occurrences
why: Untrusted tool input reaching a shell yields remote code execution.
fix: Never pass tool arguments to a shell; use argv arrays with shell disabled.
✗ No dynamic code execution — 1 occurrence
Secrets & credentials
✗ No committed secrets — 1 secret type(s) found
Tool integrity
✗ Tool descriptions free of injection markers — 1 marker(s) across 1 tool(s)
✗ No over-broad / destructive tools — 1 over-broad tool(s)
Each failed control includes:
- Why it matters: The attack vector or risk
- How to fix it: Concrete remediation steps
- Occurrences: File paths and line numbers (when available)
Architecture Trade-offs
| Component | Approach | Trade-off |
|---|---|---|
| Code analysis | Semgrep (static rules) | Fast, deterministic, but misses runtime behavior |
| Secret detection | Gitleaks (entropy + patterns) | High recall, moderate false positives on test data |
| Tool analysis | Custom regex + JSON schema parsing | Catches obvious markers, misses sophisticated prompt injections |
| Scoring | Worst-case capping | Penalizes single critical flaws, ignores partial mitigations |
| Live endpoint scanning | Optional MCP introspection | Requires API key, only sees tools exposed at runtime |
The scanner is deterministic by design. It does not use an LLM to evaluate tool safety because that would introduce non-reproducible results and new attack surfaces (prompt injection against the scanner itself).
The worst-case capping model is controversial. A server with one command-injection sink and 28 passing controls scores 7/100. The rationale: a single RCE vulnerability makes every other control irrelevant. Critics argue this discourages incremental improvement. The counter-argument: security is not additive.
CI Integration
Exit codes make the CLI a one-line quality gate:
0: Scan passed (score meets threshold)1: Scan failed (score below threshold)2: Scan error (network failure, missing dependencies)
- name: Scan MCP server
run: npx canopii scan --local . --min-grade B
The --min-grade flag sets the threshold (A+, A, B, C, D, F). Most teams use B (score ≥ 70) for production and C (score ≥ 50) for development.
False Positives and Tuning
The scanner has three confidence levels:
- High (≥ 75%): Multiple signals confirm the issue
- Medium (50-74%): Single signal, or conflicting evidence
- Low (< 50%): Heuristic match only
Prompt-injection markers have the highest false-positive rate. A tool description that says “this tool ignores invalid input” triggers the “ignore” keyword. The scanner flags it as medium confidence.
You can suppress specific findings with a .canopii.yml config:
ignore:
- rule: tool.no_injection_markers
path: src/tools/validator.ts
reason: "Legitimate use of 'ignore' in validation context"
Suppressions are auditable and version-controlled. They do not affect the public registry score (only local scans).
Limitations
The scanner cannot detect:
- Logic bugs: A tool that deletes the wrong file due to off-by-one errors
- Runtime injection: A tool that constructs shell commands dynamically based on external state
- Semantic over-privilege: A tool that has legitimate access to sensitive data but shouldn’t be exposed to an agent
- Time-of-check-to-time-of-use: A tool definition that changes between scan and deployment
Static analysis sees code, not behavior. A tool that looks safe in the repository might load malicious code from an environment variable at runtime. The scanner has no visibility into that.
Live endpoint scanning (--remote) helps but requires the server to be running and reachable. It also only sees tools that are exposed at scan time. A server that conditionally registers tools based on configuration will show different results in different environments.
Technical Verdict
Use Canopii CLI when:
- You install MCP servers from public registries or third-party repositories
- You need a pre-deployment security gate for agent tools
- You want continuous monitoring of the MCP supply chain
- You build internal MCP servers and need a quality baseline
Avoid it when:
- You need runtime behavior analysis (use dynamic testing or sandboxing instead)
- You have custom security rules that don’t map to the built-in controls (fork and extend)
- You trust the MCP server authors implicitly (but you probably shouldn’t)
The scanner is most effective as part of a layered defense: static analysis before deployment, runtime sandboxing during execution, and observability to detect anomalies. It does not replace those layers. It catches the obvious mistakes before they reach production.
The registry-wide scanning is the differentiator. Most teams scan their own code. Few teams scan every dependency. Canopii makes that default behavior for MCP servers.