shieldpi-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShieldPi (support@shieldpi.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: shieldpi-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for ShieldPi Watchtower — query 27,000+ LLM attack techniques, run scans, fetch breach forensics from any MCP-compatible client (Claude Desktop, Claude Code, Cursor).
5
+ Project-URL: Homepage, https://shieldpi.io
6
+ Project-URL: Documentation, https://github.com/ShieldPi1/shieldpi-watchtower/tree/main/mcp-server
7
+ Project-URL: Repository, https://github.com/ShieldPi1/shieldpi-watchtower
8
+ Project-URL: Issues, https://github.com/ShieldPi1/shieldpi-watchtower/issues
9
+ Project-URL: Leaderboard, https://shieldpi.info
10
+ Project-URL: Methodology, https://shieldpi.io/methodology
11
+ Author-email: ShieldPi <support@shieldpi.io>
12
+ License: MIT
13
+ License-File: LICENSE
14
+ Keywords: ai-security,anthropic,claude,jailbreak,llm-security,mcp,owasp,prompt-injection,red-team
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Information Technology
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Security
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: mcp>=1.2.0
29
+ Requires-Dist: pydantic>=2.0.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
32
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.5; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # shieldpi-mcp
38
+
39
+ > **MCP server for [ShieldPi Watchtower](https://shieldpi.io)** — query 27,000+ LLM attack techniques, run scans, and pull breach forensics from any MCP-compatible client (Claude Desktop, Claude Code, Cursor, Continue).
40
+
41
+ [![PyPI](https://img.shields.io/pypi/v/shieldpi-mcp.svg)](https://pypi.org/project/shieldpi-mcp/)
42
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
43
+ [![Leaderboard](https://img.shields.io/badge/leaderboard-shieldpi.info-22c55e)](https://shieldpi.info)
44
+
45
+ ShieldPi is the security forensics platform for LLMs and agents. It runs **27,024+ attack techniques** across 4 scan modes (Browser / API / Agent / Model), classifies findings on the **ExploitDepth L1–L4 scale**, and produces a forensic kill-chain narrative + extracted breach evidence (credentials, PII, code, tools, blast radius) for every successful attack.
46
+
47
+ This MCP server lets you talk to ShieldPi from inside any LLM session.
48
+
49
+ ## What you get
50
+
51
+ | Tier | Tool | API key? | Use it for |
52
+ |------|------|----------|-----------|
53
+ | 1 | `get_methodology` | no | Pull the V7 scoring + dedup + judge methodology |
54
+ | 1 | `get_attack_graph` | no | Cross-customer success-rate graph by technique × model family |
55
+ | 1 | `get_model_families` | no | ShieldPi's family taxonomy + similarity edges |
56
+ | 1 | `get_leaderboard_feed` | no | Live data behind shieldpi.info |
57
+ | 1 | `get_leaderboard` | no | Top models by best security score |
58
+ | 1 | `get_model_registry` | no | All 38+ models ShieldPi can test |
59
+ | 2 | `list_attack_categories` | yes | The 15 categories + OWASP mappings |
60
+ | 2 | `list_attack_techniques` | yes | Browse the 27k catalog with filters |
61
+ | 2 | `start_scan` | yes | Kick off a scan against a target |
62
+ | 2 | `get_scan_intelligence` | yes | Pull the breach-forensics package for a scan |
63
+
64
+ Tier-1 works the moment you install. Tier-2 needs a free ShieldPi API key — get one at [shieldpi.io/dashboard/api-keys](https://shieldpi.io/dashboard/api-keys).
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install shieldpi-mcp
70
+ ```
71
+
72
+ Or with `uv`:
73
+
74
+ ```bash
75
+ uv pip install shieldpi-mcp
76
+ ```
77
+
78
+ The package ships a `shieldpi-mcp` console entry point that runs the server over stdio — that's what every MCP client expects.
79
+
80
+ ## Configure your client
81
+
82
+ ### Claude Desktop
83
+
84
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "shieldpi": {
90
+ "command": "shieldpi-mcp",
91
+ "env": {
92
+ "SHIELDPI_API_KEY": "shpi_live_..."
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ Restart Claude Desktop. Look for the 🔌 plug icon — `shieldpi` should be listed with 10 tools.
100
+
101
+ ### Claude Code
102
+
103
+ ```bash
104
+ claude mcp add shieldpi -- shieldpi-mcp
105
+ ```
106
+
107
+ Then set the env var in the same shell or in your `.envrc`:
108
+
109
+ ```bash
110
+ export SHIELDPI_API_KEY="shpi_live_..."
111
+ ```
112
+
113
+ ### Cursor
114
+
115
+ Add to `~/.cursor/mcp.json`:
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "shieldpi": {
121
+ "command": "shieldpi-mcp",
122
+ "env": { "SHIELDPI_API_KEY": "shpi_live_..." }
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ ### Continue (VS Code / JetBrains)
129
+
130
+ In `~/.continue/config.json`:
131
+
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "shieldpi": {
136
+ "command": "shieldpi-mcp",
137
+ "env": { "SHIELDPI_API_KEY": "shpi_live_..." }
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Try it
144
+
145
+ Once installed and configured, ask your LLM:
146
+
147
+ > *"Using the shieldpi tools, what's the current security ranking of Claude Sonnet 4.5 vs GPT-4o?"*
148
+
149
+ > *"Pull the ShieldPi methodology and explain ExploitDepth scoring in plain English."*
150
+
151
+ > *"Browse the prompt_injection category in the ShieldPi attack catalog and pick 3 techniques worth testing on my agent."*
152
+
153
+ > *"Start a model-mode scan against `anthropic/claude-sonnet-4.5` and report when it's done."*
154
+
155
+ ## Environment variables
156
+
157
+ | Variable | Default | Purpose |
158
+ |----------|---------|---------|
159
+ | `SHIELDPI_API_KEY` | (none) | Required for tier-2 tools (techniques catalog, scans). Free tier works. |
160
+ | `SHIELDPI_API_BASE` | `https://api.shieldpi.io` | Override for self-hosted ShieldPi or staging. |
161
+ | `SHIELDPI_TIMEOUT` | `30` | Per-request timeout in seconds. |
162
+
163
+ ## Develop
164
+
165
+ ```bash
166
+ git clone https://github.com/ShieldPi1/shieldpi-watchtower.git
167
+ cd shieldpi-watchtower/mcp-server
168
+ pip install -e ".[dev]"
169
+ pytest
170
+ ```
171
+
172
+ The test suite hits live `api.shieldpi.io` for tier-1 endpoints and skips tier-2 unless `SHIELDPI_API_KEY` is set.
173
+
174
+ ## Architecture
175
+
176
+ ```
177
+ ┌─────────────────────────────────────────────────────────────────┐
178
+ │ Claude Desktop / Claude Code / Cursor │
179
+ │ (MCP Client over stdio) │
180
+ └──────────────────────────────┬──────────────────────────────────┘
181
+ │ MCP (JSON-RPC over stdio)
182
+ ▼
183
+ ┌─────────────────────────────────────────────────────────────────┐
184
+ │ shieldpi-mcp (Python) │
185
+ │ ┌──────────────┐ ┌─────────────────┐ │
186
+ │ │ 10 MCP │ │ ShieldPiClient │ │
187
+ │ │ tools │ │ (httpx) │ │
188
+ │ └──────┬───────┘ └────────┬────────┘ │
189
+ └─────────┼───────────────────┼────────────────────────────────────┘
190
+ │ │ HTTPS outbound only
191
+ │ ▼
192
+ │ ┌─────────────────────────────┐
193
+ │ │ https://api.shieldpi.io │
194
+ │ │ (FastAPI on Hetzner) │
195
+ │ └─────────────────────────────┘
196
+ ▼
197
+ stdout (MCP responses)
198
+ ```
199
+
200
+ Outbound HTTPS only. No inbound ports. API keys are read from env vars, never logged. The server is single-process, stateless across requests — safe to drop into any client.
201
+
202
+ ## License
203
+
204
+ MIT. See [LICENSE](LICENSE).
205
+
206
+ ## Links
207
+
208
+ - **Product:** [shieldpi.io](https://shieldpi.io)
209
+ - **Leaderboard:** [shieldpi.info](https://shieldpi.info)
210
+ - **Methodology:** [shieldpi.io/methodology](https://shieldpi.io/methodology)
211
+ - **Research:** [shieldpi.io/research](https://shieldpi.io/research)
212
+ - **Issues:** [github.com/ShieldPi1/shieldpi-watchtower/issues](https://github.com/ShieldPi1/shieldpi-watchtower/issues)
@@ -0,0 +1,176 @@
1
+ # shieldpi-mcp
2
+
3
+ > **MCP server for [ShieldPi Watchtower](https://shieldpi.io)** — query 27,000+ LLM attack techniques, run scans, and pull breach forensics from any MCP-compatible client (Claude Desktop, Claude Code, Cursor, Continue).
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/shieldpi-mcp.svg)](https://pypi.org/project/shieldpi-mcp/)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![Leaderboard](https://img.shields.io/badge/leaderboard-shieldpi.info-22c55e)](https://shieldpi.info)
8
+
9
+ ShieldPi is the security forensics platform for LLMs and agents. It runs **27,024+ attack techniques** across 4 scan modes (Browser / API / Agent / Model), classifies findings on the **ExploitDepth L1–L4 scale**, and produces a forensic kill-chain narrative + extracted breach evidence (credentials, PII, code, tools, blast radius) for every successful attack.
10
+
11
+ This MCP server lets you talk to ShieldPi from inside any LLM session.
12
+
13
+ ## What you get
14
+
15
+ | Tier | Tool | API key? | Use it for |
16
+ |------|------|----------|-----------|
17
+ | 1 | `get_methodology` | no | Pull the V7 scoring + dedup + judge methodology |
18
+ | 1 | `get_attack_graph` | no | Cross-customer success-rate graph by technique × model family |
19
+ | 1 | `get_model_families` | no | ShieldPi's family taxonomy + similarity edges |
20
+ | 1 | `get_leaderboard_feed` | no | Live data behind shieldpi.info |
21
+ | 1 | `get_leaderboard` | no | Top models by best security score |
22
+ | 1 | `get_model_registry` | no | All 38+ models ShieldPi can test |
23
+ | 2 | `list_attack_categories` | yes | The 15 categories + OWASP mappings |
24
+ | 2 | `list_attack_techniques` | yes | Browse the 27k catalog with filters |
25
+ | 2 | `start_scan` | yes | Kick off a scan against a target |
26
+ | 2 | `get_scan_intelligence` | yes | Pull the breach-forensics package for a scan |
27
+
28
+ Tier-1 works the moment you install. Tier-2 needs a free ShieldPi API key — get one at [shieldpi.io/dashboard/api-keys](https://shieldpi.io/dashboard/api-keys).
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install shieldpi-mcp
34
+ ```
35
+
36
+ Or with `uv`:
37
+
38
+ ```bash
39
+ uv pip install shieldpi-mcp
40
+ ```
41
+
42
+ The package ships a `shieldpi-mcp` console entry point that runs the server over stdio — that's what every MCP client expects.
43
+
44
+ ## Configure your client
45
+
46
+ ### Claude Desktop
47
+
48
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "shieldpi": {
54
+ "command": "shieldpi-mcp",
55
+ "env": {
56
+ "SHIELDPI_API_KEY": "shpi_live_..."
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Restart Claude Desktop. Look for the 🔌 plug icon — `shieldpi` should be listed with 10 tools.
64
+
65
+ ### Claude Code
66
+
67
+ ```bash
68
+ claude mcp add shieldpi -- shieldpi-mcp
69
+ ```
70
+
71
+ Then set the env var in the same shell or in your `.envrc`:
72
+
73
+ ```bash
74
+ export SHIELDPI_API_KEY="shpi_live_..."
75
+ ```
76
+
77
+ ### Cursor
78
+
79
+ Add to `~/.cursor/mcp.json`:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "shieldpi": {
85
+ "command": "shieldpi-mcp",
86
+ "env": { "SHIELDPI_API_KEY": "shpi_live_..." }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Continue (VS Code / JetBrains)
93
+
94
+ In `~/.continue/config.json`:
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "shieldpi": {
100
+ "command": "shieldpi-mcp",
101
+ "env": { "SHIELDPI_API_KEY": "shpi_live_..." }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ## Try it
108
+
109
+ Once installed and configured, ask your LLM:
110
+
111
+ > *"Using the shieldpi tools, what's the current security ranking of Claude Sonnet 4.5 vs GPT-4o?"*
112
+
113
+ > *"Pull the ShieldPi methodology and explain ExploitDepth scoring in plain English."*
114
+
115
+ > *"Browse the prompt_injection category in the ShieldPi attack catalog and pick 3 techniques worth testing on my agent."*
116
+
117
+ > *"Start a model-mode scan against `anthropic/claude-sonnet-4.5` and report when it's done."*
118
+
119
+ ## Environment variables
120
+
121
+ | Variable | Default | Purpose |
122
+ |----------|---------|---------|
123
+ | `SHIELDPI_API_KEY` | (none) | Required for tier-2 tools (techniques catalog, scans). Free tier works. |
124
+ | `SHIELDPI_API_BASE` | `https://api.shieldpi.io` | Override for self-hosted ShieldPi or staging. |
125
+ | `SHIELDPI_TIMEOUT` | `30` | Per-request timeout in seconds. |
126
+
127
+ ## Develop
128
+
129
+ ```bash
130
+ git clone https://github.com/ShieldPi1/shieldpi-watchtower.git
131
+ cd shieldpi-watchtower/mcp-server
132
+ pip install -e ".[dev]"
133
+ pytest
134
+ ```
135
+
136
+ The test suite hits live `api.shieldpi.io` for tier-1 endpoints and skips tier-2 unless `SHIELDPI_API_KEY` is set.
137
+
138
+ ## Architecture
139
+
140
+ ```
141
+ ┌─────────────────────────────────────────────────────────────────┐
142
+ │ Claude Desktop / Claude Code / Cursor │
143
+ │ (MCP Client over stdio) │
144
+ └──────────────────────────────┬──────────────────────────────────┘
145
+ │ MCP (JSON-RPC over stdio)
146
+ ▼
147
+ ┌─────────────────────────────────────────────────────────────────┐
148
+ │ shieldpi-mcp (Python) │
149
+ │ ┌──────────────┐ ┌─────────────────┐ │
150
+ │ │ 10 MCP │ │ ShieldPiClient │ │
151
+ │ │ tools │ │ (httpx) │ │
152
+ │ └──────┬───────┘ └────────┬────────┘ │
153
+ └─────────┼───────────────────┼────────────────────────────────────┘
154
+ │ │ HTTPS outbound only
155
+ │ ▼
156
+ │ ┌─────────────────────────────┐
157
+ │ │ https://api.shieldpi.io │
158
+ │ │ (FastAPI on Hetzner) │
159
+ │ └─────────────────────────────┘
160
+ ▼
161
+ stdout (MCP responses)
162
+ ```
163
+
164
+ Outbound HTTPS only. No inbound ports. API keys are read from env vars, never logged. The server is single-process, stateless across requests — safe to drop into any client.
165
+
166
+ ## License
167
+
168
+ MIT. See [LICENSE](LICENSE).
169
+
170
+ ## Links
171
+
172
+ - **Product:** [shieldpi.io](https://shieldpi.io)
173
+ - **Leaderboard:** [shieldpi.info](https://shieldpi.info)
174
+ - **Methodology:** [shieldpi.io/methodology](https://shieldpi.io/methodology)
175
+ - **Research:** [shieldpi.io/research](https://shieldpi.io/research)
176
+ - **Issues:** [github.com/ShieldPi1/shieldpi-watchtower/issues](https://github.com/ShieldPi1/shieldpi-watchtower/issues)
@@ -0,0 +1,112 @@
1
+ # Distribution checklist — shieldpi-mcp
2
+
3
+ End-to-end checklist for getting `shieldpi-mcp` in front of every Claude Desktop / Code / Cursor / Continue user. Each step is small; the order matters.
4
+
5
+ ## 1. PyPI publish (10 min — do this first)
6
+
7
+ The `publish-mcp.yml` GitHub Action publishes on a `mcp-v*` tag push, but it needs a PyPI token first.
8
+
9
+ ### One-time PyPI setup
10
+
11
+ 1. Sign in (or create account) at https://pypi.org/ as `support@shieldpi.io`.
12
+ 2. Reserve the project name: https://pypi.org/manage/account/projects/ → it'll auto-claim on first upload, but you can manually create it.
13
+ 3. Create an API token scoped to `shieldpi-mcp` only:
14
+ - https://pypi.org/manage/account/token/
15
+ - Token name: `shieldpi-mcp-github-actions`
16
+ - Scope: project `shieldpi-mcp` (after first upload). Until first upload, scope it to "Entire account" temporarily.
17
+ 4. Add the token to the repo as a secret named `PYPI_MCP_TOKEN`:
18
+ - https://github.com/ShieldPi1/shieldpi-watchtower/settings/secrets/actions
19
+ - Name: `PYPI_MCP_TOKEN` · Value: the `pypi-...` token from step 3.
20
+
21
+ ### First publish
22
+
23
+ ```bash
24
+ cd /Users/calvin/shieldpi-deploy
25
+ git tag mcp-v0.1.0
26
+ git push origin mcp-v0.1.0
27
+ ```
28
+
29
+ GitHub Actions runs `publish-mcp.yml` → tests → `python -m build` → `twine upload`. Within ~3 min the package appears at https://pypi.org/project/shieldpi-mcp/.
30
+
31
+ After the first successful upload, narrow the PyPI token scope from "Entire account" to "shieldpi-mcp" only, and rotate `PYPI_MCP_TOKEN` to the narrowed token.
32
+
33
+ ### Verify
34
+
35
+ ```bash
36
+ pip install shieldpi-mcp
37
+ shieldpi-mcp --help 2>&1 | head -3 # should print MCP server startup info
38
+ ```
39
+
40
+ ## 2. Anthropic MCP directory (1–4 weeks review)
41
+
42
+ Anthropic maintains a curated list at https://github.com/modelcontextprotocol/servers. Submission is a PR to that repo.
43
+
44
+ ### Submission steps
45
+
46
+ 1. Fork https://github.com/modelcontextprotocol/servers.
47
+ 2. Add an entry to `README.md` in the **"Third-Party Servers"** → **"Official Integrations"** (since we're an established product) or **"Community Servers"** section. Format follows the existing entries:
48
+
49
+ ```markdown
50
+ - **[ShieldPi](https://github.com/ShieldPi1/shieldpi-watchtower/tree/main/mcp-server)** — Query 27,000+ LLM attack techniques, run security scans, and pull breach forensics. Powers the [ShieldPi Watchtower](https://shieldpi.io) AI security platform.
51
+ ```
52
+
53
+ 3. Open PR with a short description:
54
+ > Adding `shieldpi-mcp` — production MCP server for ShieldPi Watchtower (LLM security forensics platform). 10 tools across 6 public + 4 authenticated endpoints. MIT-licensed, on PyPI as `shieldpi-mcp`. Tested against live api.shieldpi.io. Public methodology + leaderboard at shieldpi.io and shieldpi.info.
55
+
56
+ 4. Anthropic policy review (per their MCP directory policy: https://support.anthropic.com/en/articles/11697096):
57
+ - Outbound HTTPS only ✅
58
+ - API keys never logged ✅
59
+ - Private IPs not exposed ✅
60
+ - Clear error messages ✅
61
+ - LICENSE present (MIT) ✅
62
+ - README documents tools + auth ✅
63
+
64
+ 5. Approval typically takes 1–4 weeks. They may ask for changes — respond fast.
65
+
66
+ ### Smithery (parallel — same day)
67
+
68
+ Smithery (https://smithery.ai/) is a community MCP marketplace that auto-discovers from GitHub. Listing happens automatically once the repo is public + tagged. You can also submit manually at https://smithery.ai/new for fast indexing.
69
+
70
+ ## 3. ClawHub (OpenClaw skill registry — same day)
71
+
72
+ OpenClaw's ClawHub at https://clawhub.ai accepts MCP servers as well. Submit at https://clawhub.ai/submit.
73
+
74
+ ## 4. Inbound funnel — wire the MCP install URL into shieldpi.io
75
+
76
+ After PyPI publish:
77
+
78
+ 1. Add a "Connect via MCP" CTA on shieldpi.io homepage hero — single line:
79
+ ```
80
+ pip install shieldpi-mcp && shieldpi-mcp
81
+ ```
82
+ 2. Add a `/mcp` page documenting the 10 tools (mirror the README).
83
+ 3. Update the leaderboard footer at shieldpi.info: "Use this data in your own LLM session — `pip install shieldpi-mcp`".
84
+ 4. Add `MCP server: pip install shieldpi-mcp` to every blog post sidebar.
85
+
86
+ ## 5. Distribution announcement (next sprint, after PyPI is live)
87
+
88
+ - **HN Show post:** "Show HN: shieldpi-mcp — query 27,000+ LLM jailbreaks from inside Claude Desktop"
89
+ - **LinkedIn post** (Calvin) — focus on the use case ("ask Claude about the security posture of any model, get a real answer from a real scanner")
90
+ - **X thread** — 5 tweets walking through the 10 tools with a real example each
91
+ - **Reddit:** r/LocalLLaMA, r/MachineLearning, r/cybersecurity (read each sub's rules first; some require account history)
92
+ - **Direct outreach:** any AI security researcher who's ever cited a leaderboard
93
+
94
+ ## Timeline (optimistic)
95
+
96
+ | Day | Item |
97
+ |-----|------|
98
+ | Today | Code shipped to GitHub ✅ (commit `ae839f8`) |
99
+ | Today | PyPI token set up + first tag → PyPI publish |
100
+ | Day +1 | Anthropic MCP directory PR opened |
101
+ | Day +1 | Smithery + ClawHub submissions |
102
+ | Day +1 | shieldpi.io `/mcp` page + homepage CTA |
103
+ | Day +2–3 | HN Show + LinkedIn + X thread |
104
+ | Day +7 to +28 | Anthropic directory approval lands |
105
+ | Day +30 | First inbound usage signal (PyPI download count, ShieldPi API key signups attributable to MCP) |
106
+
107
+ ## Success metrics (90 days)
108
+
109
+ Per [[decision-shieldpi-v8-growth-adoption]]:
110
+ - 1,000+ PyPI installs
111
+ - 10+ inbound enterprise inquiries with MCP attribution
112
+ - Listed on Anthropic MCP directory
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Add ShieldPi MCP server to Claude Code.
3
+ #
4
+ # Prereq: pip install shieldpi-mcp (or pipx install shieldpi-mcp)
5
+ #
6
+ # Usage: ./claude-code.sh
7
+
8
+ set -euo pipefail
9
+
10
+ if ! command -v shieldpi-mcp >/dev/null 2>&1; then
11
+ echo "shieldpi-mcp not found. Install with: pip install shieldpi-mcp"
12
+ exit 1
13
+ fi
14
+
15
+ claude mcp add shieldpi -- shieldpi-mcp
16
+ echo
17
+ echo "Added. Tier-2 tools (techniques catalog, scans) need an API key."
18
+ echo "Set it in your shell or in ~/.envrc:"
19
+ echo " export SHIELDPI_API_KEY=\"shpi_live_...\""
20
+ echo
21
+ echo "Get a free key: https://shieldpi.io/dashboard/api-keys"
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "shieldpi": {
4
+ "command": "shieldpi-mcp",
5
+ "env": {
6
+ "SHIELDPI_API_KEY": "shpi_live_REPLACE_ME"
7
+ }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "shieldpi": {
4
+ "command": "shieldpi-mcp",
5
+ "env": {
6
+ "SHIELDPI_API_KEY": "shpi_live_REPLACE_ME"
7
+ }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "shieldpi-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for ShieldPi Watchtower — query 27,000+ LLM attack techniques, run scans, fetch breach forensics from any MCP-compatible client (Claude Desktop, Claude Code, Cursor)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "ShieldPi", email = "support@shieldpi.io" }
14
+ ]
15
+ keywords = [
16
+ "mcp",
17
+ "llm-security",
18
+ "ai-security",
19
+ "red-team",
20
+ "jailbreak",
21
+ "prompt-injection",
22
+ "owasp",
23
+ "anthropic",
24
+ "claude"
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Intended Audience :: Developers",
29
+ "Intended Audience :: Information Technology",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Operating System :: OS Independent",
32
+ "Programming Language :: Python :: 3",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Topic :: Security",
37
+ "Topic :: Software Development :: Libraries :: Python Modules"
38
+ ]
39
+ dependencies = [
40
+ "mcp>=1.2.0",
41
+ "httpx>=0.27.0",
42
+ "pydantic>=2.0.0"
43
+ ]
44
+
45
+ [project.optional-dependencies]
46
+ dev = [
47
+ "pytest>=8.0",
48
+ "pytest-asyncio>=0.23",
49
+ "pytest-httpx>=0.30",
50
+ "ruff>=0.5"
51
+ ]
52
+
53
+ [project.scripts]
54
+ shieldpi-mcp = "shieldpi_mcp.server:main"
55
+
56
+ [project.urls]
57
+ Homepage = "https://shieldpi.io"
58
+ Documentation = "https://github.com/ShieldPi1/shieldpi-watchtower/tree/main/mcp-server"
59
+ Repository = "https://github.com/ShieldPi1/shieldpi-watchtower"
60
+ Issues = "https://github.com/ShieldPi1/shieldpi-watchtower/issues"
61
+ Leaderboard = "https://shieldpi.info"
62
+ Methodology = "https://shieldpi.io/methodology"
63
+
64
+ [tool.hatch.build.targets.wheel]
65
+ packages = ["shieldpi_mcp"]
66
+
67
+ [tool.ruff]
68
+ line-length = 100
69
+ target-version = "py310"
@@ -0,0 +1,3 @@
1
+ """ShieldPi MCP — Model Context Protocol server for ShieldPi Watchtower."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow `python -m shieldpi_mcp` to launch the server."""
2
+
3
+ from shieldpi_mcp.server import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,104 @@
1
+ """Async HTTP client for api.shieldpi.io.
2
+
3
+ Wraps the public + authenticated REST surface so MCP tools can call it without
4
+ each one re-implementing auth, retries, and error formatting.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+ DEFAULT_BASE_URL = os.getenv("SHIELDPI_API_BASE", "https://api.shieldpi.io")
15
+ DEFAULT_TIMEOUT = float(os.getenv("SHIELDPI_TIMEOUT", "30"))
16
+ USER_AGENT = "shieldpi-mcp/0.1.0 (+https://shieldpi.io)"
17
+
18
+
19
+ class ShieldPiAPIError(RuntimeError):
20
+ """Raised when api.shieldpi.io returns a non-2xx response."""
21
+
22
+ def __init__(self, status: int, body: str, url: str) -> None:
23
+ super().__init__(f"ShieldPi API {status} on {url}: {body[:300]}")
24
+ self.status = status
25
+ self.body = body
26
+ self.url = url
27
+
28
+
29
+ class ShieldPiClient:
30
+ """Thin async wrapper around api.shieldpi.io.
31
+
32
+ Public endpoints work without an API key. Authenticated endpoints require
33
+ `SHIELDPI_API_KEY` (set in env or passed to the constructor) — the client
34
+ sends it as both `X-API-Key` and `Authorization: Bearer` because different
35
+ routes accept different conventions.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ api_key: str | None = None,
41
+ base_url: str | None = None,
42
+ timeout: float | None = None,
43
+ ) -> None:
44
+ self.api_key = api_key or os.getenv("SHIELDPI_API_KEY")
45
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
46
+ self.timeout = timeout or DEFAULT_TIMEOUT
47
+ self._client: httpx.AsyncClient | None = None
48
+
49
+ async def __aenter__(self) -> "ShieldPiClient":
50
+ self._client = httpx.AsyncClient(
51
+ base_url=self.base_url,
52
+ timeout=self.timeout,
53
+ headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
54
+ )
55
+ return self
56
+
57
+ async def __aexit__(self, *_: Any) -> None:
58
+ if self._client is not None:
59
+ await self._client.aclose()
60
+ self._client = None
61
+
62
+ def _auth_headers(self) -> dict[str, str]:
63
+ if not self.api_key:
64
+ return {}
65
+ return {
66
+ "X-API-Key": self.api_key,
67
+ "Authorization": f"Bearer {self.api_key}",
68
+ }
69
+
70
+ def require_key(self, tool_name: str) -> None:
71
+ if not self.api_key:
72
+ raise ShieldPiAPIError(
73
+ status=401,
74
+ body=(
75
+ f"Tool `{tool_name}` requires a ShieldPi API key. "
76
+ f"Set SHIELDPI_API_KEY in your MCP client config "
77
+ f"(get one at https://shieldpi.io/dashboard/api-keys)."
78
+ ),
79
+ url=f"{self.base_url}/_local",
80
+ )
81
+
82
+ async def get(
83
+ self, path: str, params: dict[str, Any] | None = None, auth: bool = False
84
+ ) -> Any:
85
+ assert self._client is not None, "ShieldPiClient must be used as `async with`"
86
+ headers = self._auth_headers() if auth else {}
87
+ resp = await self._client.get(path, params=params, headers=headers)
88
+ return self._parse(resp)
89
+
90
+ async def post(
91
+ self, path: str, json: dict[str, Any] | None = None, auth: bool = True
92
+ ) -> Any:
93
+ assert self._client is not None, "ShieldPiClient must be used as `async with`"
94
+ headers = self._auth_headers() if auth else {}
95
+ resp = await self._client.post(path, json=json, headers=headers)
96
+ return self._parse(resp)
97
+
98
+ @staticmethod
99
+ def _parse(resp: httpx.Response) -> Any:
100
+ if resp.status_code >= 400:
101
+ raise ShieldPiAPIError(resp.status_code, resp.text, str(resp.url))
102
+ if resp.headers.get("content-type", "").startswith("application/json"):
103
+ return resp.json()
104
+ return resp.text
@@ -0,0 +1,251 @@
1
+ """ShieldPi MCP server.
2
+
3
+ Exposes 10 tools that wrap api.shieldpi.io so any MCP-compatible client
4
+ (Claude Desktop, Claude Code, Cursor, Continue, etc.) can:
5
+
6
+ - Query the ShieldPi V7 methodology, attack graph, model families, leaderboard
7
+ - Look up models in the registry + their tested security posture
8
+ - Browse the 27,000+ attack technique library by category
9
+ - Kick off scans against a target and pull back forensic results
10
+
11
+ Tier-1 tools (no API key required):
12
+ get_methodology, get_attack_graph, get_model_families,
13
+ get_leaderboard_feed, get_leaderboard, get_model_registry
14
+
15
+ Tier-2 tools (require SHIELDPI_API_KEY env var):
16
+ list_attack_categories, list_attack_techniques,
17
+ start_scan, get_scan_intelligence
18
+
19
+ Run as a stdio MCP server: `shieldpi-mcp` (entry point) or
20
+ `python -m shieldpi_mcp.server`.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from typing import Any, Literal
27
+
28
+ from mcp.server.fastmcp import FastMCP
29
+
30
+ from shieldpi_mcp.client import ShieldPiAPIError, ShieldPiClient
31
+
32
+ mcp = FastMCP(
33
+ name="shieldpi",
34
+ instructions=(
35
+ "ShieldPi is an LLM security scanner with 27,000+ attack techniques, "
36
+ "4 scan modes (Browser/API/Agent/Model), and a forensic platform that "
37
+ "extracts breach evidence from successful attacks. Use these tools to "
38
+ "look up the methodology, browse the attack catalog, query the public "
39
+ "leaderboard, and (with an API key) run scans + fetch forensic results. "
40
+ "Public docs: https://shieldpi.io/methodology · Leaderboard: https://shieldpi.info"
41
+ ),
42
+ )
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Tier-1 tools (public, no API key required)
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ @mcp.tool()
51
+ async def get_methodology() -> str:
52
+ """Return ShieldPi's full V7 scoring + dedup + judge + breach-layer methodology.
53
+
54
+ Use this when the user asks "how does ShieldPi score a model?", "what does
55
+ ExploitDepth L3 mean?", or wants to cite the methodology in a report.
56
+ Returns the same JSON document published at /api/intelligence/methodology.
57
+ """
58
+ async with ShieldPiClient() as client:
59
+ data = await client.get("/api/intelligence/methodology")
60
+ return json.dumps(data, indent=2)
61
+
62
+
63
+ @mcp.tool()
64
+ async def get_attack_graph() -> str:
65
+ """Return the cross-customer anonymized attack-success graph.
66
+
67
+ Maps attack technique → success rate per model family. The graph gets
68
+ smarter as more scans complete (network effect). Useful when the user
69
+ asks "which jailbreaks work on Claude / GPT / Gemini today?"
70
+ """
71
+ async with ShieldPiClient() as client:
72
+ data = await client.get("/api/intelligence/attack-graph")
73
+ return json.dumps(data, indent=2)
74
+
75
+
76
+ @mcp.tool()
77
+ async def get_model_families() -> str:
78
+ """Return ShieldPi's model-family taxonomy + similarity edges.
79
+
80
+ Useful when the user wants to know what families ShieldPi recognizes
81
+ (anthropic / openai / google / meta / qwen / kimi / deepseek / etc.) and
82
+ how findings transfer across families.
83
+ """
84
+ async with ShieldPiClient() as client:
85
+ data = await client.get("/api/intelligence/families")
86
+ return json.dumps(data, indent=2)
87
+
88
+
89
+ @mcp.tool()
90
+ async def get_leaderboard_feed() -> str:
91
+ """Return the live leaderboard feed — the data behind shieldpi.info.
92
+
93
+ Gives every model with a recent ShieldPi scan: best security score, grade,
94
+ last-tested timestamp, weakest category. Updated as scans complete.
95
+ """
96
+ async with ShieldPiClient() as client:
97
+ data = await client.get("/api/intelligence/leaderboard-feed")
98
+ return json.dumps(data, indent=2)
99
+
100
+
101
+ @mcp.tool()
102
+ async def get_leaderboard(limit: int = 30) -> str:
103
+ """Return the public model leaderboard sorted by best security score.
104
+
105
+ Args:
106
+ limit: max number of models to return (default 30, capped server-side).
107
+ """
108
+ async with ShieldPiClient() as client:
109
+ data = await client.get("/api/models/leaderboard", params={"limit": limit})
110
+ return json.dumps(data, indent=2)
111
+
112
+
113
+ @mcp.tool()
114
+ async def get_model_registry(family: str | None = None) -> str:
115
+ """Return the registry of models ShieldPi knows how to test.
116
+
117
+ Args:
118
+ family: optional family filter (anthropic, openai, google, meta, qwen, ...).
119
+ When omitted, returns all 38+ supported models.
120
+ """
121
+ async with ShieldPiClient() as client:
122
+ data = await client.get("/api/models/registry")
123
+ if family:
124
+ # Server doesn't filter; do it client-side so the LLM only sees what it asked for.
125
+ if isinstance(data, dict) and "models" in data:
126
+ data = {
127
+ **data,
128
+ "models": [
129
+ m for m in data["models"]
130
+ if m.get("family", "").lower() == family.lower()
131
+ ],
132
+ }
133
+ return json.dumps(data, indent=2)
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Tier-2 tools (require API key)
138
+ # ---------------------------------------------------------------------------
139
+
140
+
141
+ @mcp.tool()
142
+ async def list_attack_categories() -> str:
143
+ """List the 15 attack categories ShieldPi tests against.
144
+
145
+ Requires SHIELDPI_API_KEY (free tier works). Each category includes the
146
+ technique count, OWASP LLM mapping, and OWASP Agentic mapping.
147
+ """
148
+ async with ShieldPiClient() as client:
149
+ client.require_key("list_attack_categories")
150
+ data = await client.get("/api/attacks/categories", auth=True)
151
+ return json.dumps(data, indent=2)
152
+
153
+
154
+ @mcp.tool()
155
+ async def list_attack_techniques(
156
+ category: str | None = None,
157
+ source: str | None = None,
158
+ limit: int = 25,
159
+ offset: int = 0,
160
+ ) -> str:
161
+ """Browse the 27,000+ attack technique library.
162
+
163
+ Args:
164
+ category: optional category filter (e.g. "prompt_injection", "jailbreak",
165
+ "agent_exploitation", "data_exfiltration").
166
+ source: optional source filter (e.g. "github:elder-plinius/L1B3RT45",
167
+ "shieldpi:agent_attack_library", "agentdojo").
168
+ limit: max techniques per page (default 25, max 100).
169
+ offset: pagination offset.
170
+
171
+ Requires SHIELDPI_API_KEY.
172
+ """
173
+ params: dict[str, Any] = {"limit": min(limit, 100), "offset": offset}
174
+ if category:
175
+ params["category"] = category
176
+ if source:
177
+ params["source"] = source
178
+ async with ShieldPiClient() as client:
179
+ client.require_key("list_attack_techniques")
180
+ data = await client.get("/api/attacks", params=params, auth=True)
181
+ return json.dumps(data, indent=2)
182
+
183
+
184
+ @mcp.tool()
185
+ async def start_scan(
186
+ target_url: str,
187
+ mode: Literal["browser", "api", "agent", "model"] = "browser",
188
+ target_name: str | None = None,
189
+ depth: Literal["quick", "standard", "deep"] = "standard",
190
+ ) -> str:
191
+ """Start a ShieldPi scan against a target URL.
192
+
193
+ Args:
194
+ target_url: URL or model identifier to scan. For `mode=model`, pass an
195
+ OpenRouter slug like "anthropic/claude-sonnet-4.5" or "openai/gpt-4o".
196
+ mode: scan mode — browser (web app), api (REST), agent (live agent),
197
+ model (LLM via OpenRouter). Default: browser.
198
+ target_name: human-readable label for the target. Defaults to the URL.
199
+ depth: quick (~5min, 50 techniques), standard (~30min, 500 techniques),
200
+ deep (~2h, 5000 techniques). Default: standard.
201
+
202
+ Returns the scan_id you can pass to `get_scan_intelligence`.
203
+ Requires SHIELDPI_API_KEY.
204
+ """
205
+ body = {
206
+ "target_url": target_url,
207
+ "mode": mode,
208
+ "target_name": target_name or target_url,
209
+ "depth": depth,
210
+ }
211
+ async with ShieldPiClient() as client:
212
+ client.require_key("start_scan")
213
+ data = await client.post("/api/scans", json=body, auth=True)
214
+ return json.dumps(data, indent=2)
215
+
216
+
217
+ @mcp.tool()
218
+ async def get_scan_intelligence(scan_id: str) -> str:
219
+ """Pull the full intelligence + breach-forensics package for a scan.
220
+
221
+ Returns: security score + grade + per-category results + extracted breach
222
+ artifacts (creds / PII / code / tools) + kill chain narrative + business
223
+ impact estimate.
224
+
225
+ Args:
226
+ scan_id: UUID returned by `start_scan` or visible in the dashboard URL.
227
+
228
+ Requires SHIELDPI_API_KEY.
229
+ """
230
+ async with ShieldPiClient() as client:
231
+ client.require_key("get_scan_intelligence")
232
+ data = await client.get(f"/api/scans/{scan_id}/intelligence", auth=True)
233
+ return json.dumps(data, indent=2)
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Entry point
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ def main() -> None:
242
+ """Run the MCP server over stdio (Claude Desktop / Code / Cursor default)."""
243
+ try:
244
+ mcp.run()
245
+ except ShieldPiAPIError as exc:
246
+ # FastMCP catches tool errors per-call; this only fires if the loop itself dies.
247
+ raise SystemExit(f"shieldpi-mcp fatal: {exc}") from exc
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
@@ -0,0 +1,71 @@
1
+ """Smoke tests for shieldpi-mcp.
2
+
3
+ Hits live api.shieldpi.io for tier-1 endpoints (no auth). Skips tier-2
4
+ unless SHIELDPI_API_KEY is set.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ import pytest
12
+
13
+ from shieldpi_mcp.client import ShieldPiAPIError, ShieldPiClient
14
+
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_get_methodology_returns_v7_document() -> None:
18
+ async with ShieldPiClient() as client:
19
+ data = await client.get("/api/intelligence/methodology")
20
+ assert isinstance(data, dict)
21
+ assert data.get("document_version", "").startswith("v7")
22
+ assert "breach_layer_v7" in data
23
+ assert "scoring" in data and "judge" in data and "dedup" in data
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_get_leaderboard_returns_models() -> None:
28
+ async with ShieldPiClient() as client:
29
+ data = await client.get("/api/models/leaderboard", params={"limit": 5})
30
+ assert isinstance(data, dict)
31
+ assert "models" in data
32
+ assert isinstance(data["models"], list)
33
+
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_get_attack_graph_responds() -> None:
37
+ async with ShieldPiClient() as client:
38
+ data = await client.get("/api/intelligence/attack-graph")
39
+ assert data is not None # endpoint returns 200 even with empty graph
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_tier2_without_key_raises() -> None:
44
+ # Force no-key path (env var must be unset for the test process)
45
+ client = ShieldPiClient(api_key=None)
46
+ async with client:
47
+ with pytest.raises(ShieldPiAPIError) as excinfo:
48
+ client.require_key("list_attack_categories")
49
+ assert excinfo.value.status == 401
50
+ assert "API key" in excinfo.value.body
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ @pytest.mark.skipif(
55
+ not os.getenv("SHIELDPI_API_KEY"),
56
+ reason="SHIELDPI_API_KEY not set; skipping authenticated smoke test",
57
+ )
58
+ async def test_list_attack_categories_with_key() -> None:
59
+ async with ShieldPiClient() as client:
60
+ data = await client.get("/api/attacks/categories", auth=True)
61
+ assert isinstance(data, (list, dict))
62
+
63
+
64
+ def test_server_imports_cleanly() -> None:
65
+ """Importing the FastMCP server registers all tools without error."""
66
+ from shieldpi_mcp.server import mcp # noqa: F401
67
+
68
+ # FastMCP internal: tools are stored in `_tool_manager`. Just confirm at
69
+ # least one tool was registered. The exact internal API is private; if it
70
+ # changes upstream this test gets a clear failure pointing to the upgrade.
71
+ assert hasattr(mcp, "_tool_manager") or hasattr(mcp, "tools")