ptm-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.
- ptm_mcp-0.1.0/PKG-INFO +198 -0
- ptm_mcp-0.1.0/README.md +167 -0
- ptm_mcp-0.1.0/pyproject.toml +71 -0
- ptm_mcp-0.1.0/setup.cfg +4 -0
- ptm_mcp-0.1.0/src/ptm_mcp/__init__.py +5 -0
- ptm_mcp-0.1.0/src/ptm_mcp/__main__.py +22 -0
- ptm_mcp-0.1.0/src/ptm_mcp/config.py +59 -0
- ptm_mcp-0.1.0/src/ptm_mcp/env.py +54 -0
- ptm_mcp-0.1.0/src/ptm_mcp/headers.py +48 -0
- ptm_mcp-0.1.0/src/ptm_mcp/resources.py +219 -0
- ptm_mcp-0.1.0/src/ptm_mcp/server.py +62 -0
- ptm_mcp-0.1.0/src/ptm_mcp/startup.py +159 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/__init__.py +102 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/_helpers.py +33 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/_writes.py +30 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/evals.py +195 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/optimization.py +100 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/optimization_writes.py +118 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/prompts.py +117 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/providers.py +39 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/runs.py +141 -0
- ptm_mcp-0.1.0/src/ptm_mcp/tools/versions.py +179 -0
- ptm_mcp-0.1.0/src/ptm_mcp.egg-info/PKG-INFO +198 -0
- ptm_mcp-0.1.0/src/ptm_mcp.egg-info/SOURCES.txt +26 -0
- ptm_mcp-0.1.0/src/ptm_mcp.egg-info/dependency_links.txt +1 -0
- ptm_mcp-0.1.0/src/ptm_mcp.egg-info/entry_points.txt +2 -0
- ptm_mcp-0.1.0/src/ptm_mcp.egg-info/requires.txt +11 -0
- ptm_mcp-0.1.0/src/ptm_mcp.egg-info/top_level.txt +1 -0
ptm_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ptm-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP stdio server for the Prompt Test Manager API
|
|
5
|
+
Author: PTM Team
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/15five/prompt-test-manager
|
|
8
|
+
Project-URL: Repository, https://github.com/15five/prompt-test-manager
|
|
9
|
+
Project-URL: Changelog, https://github.com/15five/prompt-test-manager/blob/main/packages/ptm-mcp/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/15five/prompt-test-manager/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: mcp<2.0,>=1.2
|
|
22
|
+
Requires-Dist: ptm-client<1.0,>=0.3.0
|
|
23
|
+
Requires-Dist: pydantic<3.0,>=2.8
|
|
24
|
+
Requires-Dist: packaging<26.0,>=24.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest<9.0,>=8.2; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.23; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov<7.0,>=5.0; extra == "dev"
|
|
29
|
+
Requires-Dist: responses<1.0,>=0.25; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff<1.0,>=0.6; extra == "dev"
|
|
31
|
+
|
|
32
|
+
# ptm-mcp
|
|
33
|
+
|
|
34
|
+
MCP (Model Context Protocol) stdio server for the Prompt Test Manager API.
|
|
35
|
+
|
|
36
|
+
Lets agents built on top of MCP-capable clients (Claude Desktop, Claude Code, Codex, Goose) call PTM as first-class tools: list prompts, run evaluations, submit optimizations. All traffic is tagged with `X-PTM-Client: ptm-mcp/<version>` + a per-process `X-PTM-MCP-Session` UUID so the PTM backend can rate-limit, budget, and audit agent traffic separately from humans and service accounts.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 1. Mint a PTM token (prompts for your PTM URL + email + password)
|
|
42
|
+
bash scripts/mint-ptm-mcp-token.sh # macOS / Linux
|
|
43
|
+
pwsh scripts/mint-ptm-mcp-token.ps1 # Windows
|
|
44
|
+
|
|
45
|
+
# 2. Wire into your MCP client of choice
|
|
46
|
+
bash scripts/install-claude-desktop.sh # Claude Desktop (macOS / Linux)
|
|
47
|
+
pwsh scripts/install-claude-desktop.ps1 # Claude Desktop (Windows)
|
|
48
|
+
|
|
49
|
+
# Claude Code - macOS / Linux
|
|
50
|
+
claude mcp add --transport stdio --scope user ptm -- uvx ptm-mcp
|
|
51
|
+
|
|
52
|
+
# Claude Code - Windows
|
|
53
|
+
claude mcp add --transport stdio --scope user ptm -- cmd /c uvx ptm-mcp
|
|
54
|
+
|
|
55
|
+
# Codex (any OS)
|
|
56
|
+
codex mcp add ptm -- uvx ptm-mcp
|
|
57
|
+
|
|
58
|
+
# Env vars can be set via `--env KEY=VAL` on either command.
|
|
59
|
+
# Full setup + token flow: docs/mcp-integration.md
|
|
60
|
+
|
|
61
|
+
# Uninstall Claude Desktop entry (creates a timestamped backup):
|
|
62
|
+
bash scripts/uninstall-claude-desktop.sh # macOS / Linux
|
|
63
|
+
pwsh scripts/uninstall-claude-desktop.ps1 # Windows
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Full setup + troubleshooting: `docs/mcp-integration.md`.
|
|
67
|
+
|
|
68
|
+
## Status
|
|
69
|
+
|
|
70
|
+
Phase 2 complete: 17 tools (canary + 12 read + 4 write), 5 resource URI patterns, read-only gate, live end-to-end integration. Ships at `0.1.0`.
|
|
71
|
+
|
|
72
|
+
## Prereqs
|
|
73
|
+
|
|
74
|
+
- Python >= 3.12 in a venv you control.
|
|
75
|
+
- PTM backend >= 1.9.0 (MCP middleware chokepoint landed in 1.9.0; older backends cannot enforce agent-scoped limits).
|
|
76
|
+
- For the stdio smoke test: Node >= 18 (for `npx @modelcontextprotocol/inspector`) or a global install of the MCP Inspector CLI.
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install ptm-mcp
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requires Python >= 3.12. `ptm-mcp` pulls in `ptm-client` and the `mcp` SDK automatically. For pinned, runtime-tested versions see `pyproject.toml` in the release tag.
|
|
85
|
+
|
|
86
|
+
### From source (dev mode)
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
git clone git@github.com:15five/prompt-test-manager
|
|
90
|
+
cd prompt-test-manager
|
|
91
|
+
python3.12 -m venv .venv && source .venv/bin/activate
|
|
92
|
+
pip install -e packages/ptm-client
|
|
93
|
+
pip install -e "packages/ptm-mcp[dev]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Dev mode is what CI exercises. When developing locally, wire your MCP client config at `PYTHONPATH=packages/ptm-mcp/src:packages/ptm-client/src` so edits in `src/` are picked up without reinstalling.
|
|
97
|
+
|
|
98
|
+
## Environment variables
|
|
99
|
+
|
|
100
|
+
Consumed at startup. Missing required values fail fast with a descriptive error.
|
|
101
|
+
|
|
102
|
+
| Variable | Required | Default | Notes |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `PTM_API_BASE_URL` | yes | - | e.g. `https://ptm.example.com` |
|
|
105
|
+
| `PTM_API_TOKEN` | yes | - | PTM bearer. Service-account tokens preferred. |
|
|
106
|
+
| `PTM_MCP_READ_ONLY` | no | `true` | Flip to `false` to unlock the 4 write tools. |
|
|
107
|
+
| `PTM_MCP_TIMEOUT_SECONDS` | no | `30` | Per-request timeout (1..600). |
|
|
108
|
+
| `PTM_MCP_LOG_LEVEL` | no | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`. |
|
|
109
|
+
|
|
110
|
+
Startup scrubs every env var outside a narrow allow-list (cloud creds, GitHub tokens, `PATH` - all get dropped). See `src/ptm_mcp/env.py`.
|
|
111
|
+
|
|
112
|
+
## Claude Desktop config snippet
|
|
113
|
+
|
|
114
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"ptm": {
|
|
120
|
+
"command": "uvx",
|
|
121
|
+
"args": ["ptm-mcp"],
|
|
122
|
+
"env": {
|
|
123
|
+
"PTM_API_BASE_URL": "http://localhost:8010",
|
|
124
|
+
"PTM_API_TOKEN": "ptm_u_PASTE_HERE",
|
|
125
|
+
"PTM_MCP_READ_ONLY": "true"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`uvx` handles Python provisioning + caching automatically; no PYTHONPATH needed when installed from PyPI. Other clients (Claude Code, Codex): see `docs/mcp-integration.md` for client-specific wire-up.
|
|
133
|
+
|
|
134
|
+
## Tool inventory
|
|
135
|
+
|
|
136
|
+
### Read (13)
|
|
137
|
+
|
|
138
|
+
`list_providers`, `list_prompts`, `get_prompt`, `get_prompt_tests`, `list_prompt_versions`, `get_prompt_version`, `compare_prompt_versions`, `list_runs`, `get_run`, `get_run_report`, `get_optimization_status`, `get_optimization_history`, `get_optimization_detail`.
|
|
139
|
+
|
|
140
|
+
### Write (4, gated by `PTM_MCP_READ_ONLY`)
|
|
141
|
+
|
|
142
|
+
`run_manual_eval`, `run_prompt_eval`, `submit_optimization`, `cancel_optimization`.
|
|
143
|
+
|
|
144
|
+
### Resources (5 URI patterns)
|
|
145
|
+
|
|
146
|
+
- `ptm://prompts/{prompt_id}` - active version's `prompt_text` (text/plain)
|
|
147
|
+
- `ptm://prompts/{prompt_id}/v{N}` - that version's `prompt_text` (text/plain)
|
|
148
|
+
- `ptm://runs/{run_key}/report.md` - markdown report (text/markdown)
|
|
149
|
+
- `ptm://runs/{run_key}/report.html` - HTML report (text/html)
|
|
150
|
+
- `ptm://optimizations/{optimization_id}/report.md` - markdown summary (text/markdown)
|
|
151
|
+
|
|
152
|
+
Dynamic segments are allow-list validated (`^[a-zA-Z0-9_.-]+$` plus explicit `.`/`..` rejection). See `docs/mcp-security.md`.
|
|
153
|
+
|
|
154
|
+
## Security defaults
|
|
155
|
+
|
|
156
|
+
- `PTM_MCP_READ_ONLY=true` blocks every write tool at call time.
|
|
157
|
+
- `X-PTM-Client` + `X-PTM-MCP-Session` on every outbound request.
|
|
158
|
+
- Env scrub at startup.
|
|
159
|
+
- Startup preflight (`/healthz` + `/auth/me` + `/meta`) with exponential backoff on transient failures and dedicated exit codes per failed layer.
|
|
160
|
+
|
|
161
|
+
Full details: `docs/mcp-security.md`.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
pip install -e packages/ptm-client
|
|
167
|
+
pip install -e 'packages/ptm-mcp[dev]'
|
|
168
|
+
PYTHONPATH=packages/ptm-mcp/src:packages/ptm-client/src \
|
|
169
|
+
pytest packages/ptm-mcp/tests -q
|
|
170
|
+
ruff check packages/ptm-mcp/src packages/ptm-mcp/tests
|
|
171
|
+
ruff format --check packages/ptm-mcp/src packages/ptm-mcp/tests
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
End-to-end smoke (requires a running backend and Node for `npx`):
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
PTM_API_BASE_URL=http://localhost:8010 \
|
|
178
|
+
PTM_API_TOKEN="ptm_u_..." \
|
|
179
|
+
bash scripts/smoke_mcp_inspector.sh
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Exit codes
|
|
183
|
+
|
|
184
|
+
| Code | Meaning |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `0` | clean shutdown |
|
|
187
|
+
| `1` | unhandled exception |
|
|
188
|
+
| `2` | `/healthz` unreachable after 31s of backoff |
|
|
189
|
+
| `3` | `/auth/me` rejected the token |
|
|
190
|
+
| `4` | backend version < 1.9.0 or unparseable |
|
|
191
|
+
| `130` | interrupted (SIGINT) |
|
|
192
|
+
|
|
193
|
+
## See also
|
|
194
|
+
|
|
195
|
+
- `docs/mcp-integration.md` - end-user setup
|
|
196
|
+
- `docs/mcp-developer.md` - adding a tool
|
|
197
|
+
- `docs/mcp-security.md` - tokens, env, headers, read-only, kill switch
|
|
198
|
+
- `docs/mcp-admin.md` - ops runbook + alert response
|
ptm_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# ptm-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) stdio server for the Prompt Test Manager API.
|
|
4
|
+
|
|
5
|
+
Lets agents built on top of MCP-capable clients (Claude Desktop, Claude Code, Codex, Goose) call PTM as first-class tools: list prompts, run evaluations, submit optimizations. All traffic is tagged with `X-PTM-Client: ptm-mcp/<version>` + a per-process `X-PTM-MCP-Session` UUID so the PTM backend can rate-limit, budget, and audit agent traffic separately from humans and service accounts.
|
|
6
|
+
|
|
7
|
+
## Quickstart
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 1. Mint a PTM token (prompts for your PTM URL + email + password)
|
|
11
|
+
bash scripts/mint-ptm-mcp-token.sh # macOS / Linux
|
|
12
|
+
pwsh scripts/mint-ptm-mcp-token.ps1 # Windows
|
|
13
|
+
|
|
14
|
+
# 2. Wire into your MCP client of choice
|
|
15
|
+
bash scripts/install-claude-desktop.sh # Claude Desktop (macOS / Linux)
|
|
16
|
+
pwsh scripts/install-claude-desktop.ps1 # Claude Desktop (Windows)
|
|
17
|
+
|
|
18
|
+
# Claude Code - macOS / Linux
|
|
19
|
+
claude mcp add --transport stdio --scope user ptm -- uvx ptm-mcp
|
|
20
|
+
|
|
21
|
+
# Claude Code - Windows
|
|
22
|
+
claude mcp add --transport stdio --scope user ptm -- cmd /c uvx ptm-mcp
|
|
23
|
+
|
|
24
|
+
# Codex (any OS)
|
|
25
|
+
codex mcp add ptm -- uvx ptm-mcp
|
|
26
|
+
|
|
27
|
+
# Env vars can be set via `--env KEY=VAL` on either command.
|
|
28
|
+
# Full setup + token flow: docs/mcp-integration.md
|
|
29
|
+
|
|
30
|
+
# Uninstall Claude Desktop entry (creates a timestamped backup):
|
|
31
|
+
bash scripts/uninstall-claude-desktop.sh # macOS / Linux
|
|
32
|
+
pwsh scripts/uninstall-claude-desktop.ps1 # Windows
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Full setup + troubleshooting: `docs/mcp-integration.md`.
|
|
36
|
+
|
|
37
|
+
## Status
|
|
38
|
+
|
|
39
|
+
Phase 2 complete: 17 tools (canary + 12 read + 4 write), 5 resource URI patterns, read-only gate, live end-to-end integration. Ships at `0.1.0`.
|
|
40
|
+
|
|
41
|
+
## Prereqs
|
|
42
|
+
|
|
43
|
+
- Python >= 3.12 in a venv you control.
|
|
44
|
+
- PTM backend >= 1.9.0 (MCP middleware chokepoint landed in 1.9.0; older backends cannot enforce agent-scoped limits).
|
|
45
|
+
- For the stdio smoke test: Node >= 18 (for `npx @modelcontextprotocol/inspector`) or a global install of the MCP Inspector CLI.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
pip install ptm-mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Python >= 3.12. `ptm-mcp` pulls in `ptm-client` and the `mcp` SDK automatically. For pinned, runtime-tested versions see `pyproject.toml` in the release tag.
|
|
54
|
+
|
|
55
|
+
### From source (dev mode)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
git clone git@github.com:15five/prompt-test-manager
|
|
59
|
+
cd prompt-test-manager
|
|
60
|
+
python3.12 -m venv .venv && source .venv/bin/activate
|
|
61
|
+
pip install -e packages/ptm-client
|
|
62
|
+
pip install -e "packages/ptm-mcp[dev]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Dev mode is what CI exercises. When developing locally, wire your MCP client config at `PYTHONPATH=packages/ptm-mcp/src:packages/ptm-client/src` so edits in `src/` are picked up without reinstalling.
|
|
66
|
+
|
|
67
|
+
## Environment variables
|
|
68
|
+
|
|
69
|
+
Consumed at startup. Missing required values fail fast with a descriptive error.
|
|
70
|
+
|
|
71
|
+
| Variable | Required | Default | Notes |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| `PTM_API_BASE_URL` | yes | - | e.g. `https://ptm.example.com` |
|
|
74
|
+
| `PTM_API_TOKEN` | yes | - | PTM bearer. Service-account tokens preferred. |
|
|
75
|
+
| `PTM_MCP_READ_ONLY` | no | `true` | Flip to `false` to unlock the 4 write tools. |
|
|
76
|
+
| `PTM_MCP_TIMEOUT_SECONDS` | no | `30` | Per-request timeout (1..600). |
|
|
77
|
+
| `PTM_MCP_LOG_LEVEL` | no | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` / `CRITICAL`. |
|
|
78
|
+
|
|
79
|
+
Startup scrubs every env var outside a narrow allow-list (cloud creds, GitHub tokens, `PATH` - all get dropped). See `src/ptm_mcp/env.py`.
|
|
80
|
+
|
|
81
|
+
## Claude Desktop config snippet
|
|
82
|
+
|
|
83
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"ptm": {
|
|
89
|
+
"command": "uvx",
|
|
90
|
+
"args": ["ptm-mcp"],
|
|
91
|
+
"env": {
|
|
92
|
+
"PTM_API_BASE_URL": "http://localhost:8010",
|
|
93
|
+
"PTM_API_TOKEN": "ptm_u_PASTE_HERE",
|
|
94
|
+
"PTM_MCP_READ_ONLY": "true"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`uvx` handles Python provisioning + caching automatically; no PYTHONPATH needed when installed from PyPI. Other clients (Claude Code, Codex): see `docs/mcp-integration.md` for client-specific wire-up.
|
|
102
|
+
|
|
103
|
+
## Tool inventory
|
|
104
|
+
|
|
105
|
+
### Read (13)
|
|
106
|
+
|
|
107
|
+
`list_providers`, `list_prompts`, `get_prompt`, `get_prompt_tests`, `list_prompt_versions`, `get_prompt_version`, `compare_prompt_versions`, `list_runs`, `get_run`, `get_run_report`, `get_optimization_status`, `get_optimization_history`, `get_optimization_detail`.
|
|
108
|
+
|
|
109
|
+
### Write (4, gated by `PTM_MCP_READ_ONLY`)
|
|
110
|
+
|
|
111
|
+
`run_manual_eval`, `run_prompt_eval`, `submit_optimization`, `cancel_optimization`.
|
|
112
|
+
|
|
113
|
+
### Resources (5 URI patterns)
|
|
114
|
+
|
|
115
|
+
- `ptm://prompts/{prompt_id}` - active version's `prompt_text` (text/plain)
|
|
116
|
+
- `ptm://prompts/{prompt_id}/v{N}` - that version's `prompt_text` (text/plain)
|
|
117
|
+
- `ptm://runs/{run_key}/report.md` - markdown report (text/markdown)
|
|
118
|
+
- `ptm://runs/{run_key}/report.html` - HTML report (text/html)
|
|
119
|
+
- `ptm://optimizations/{optimization_id}/report.md` - markdown summary (text/markdown)
|
|
120
|
+
|
|
121
|
+
Dynamic segments are allow-list validated (`^[a-zA-Z0-9_.-]+$` plus explicit `.`/`..` rejection). See `docs/mcp-security.md`.
|
|
122
|
+
|
|
123
|
+
## Security defaults
|
|
124
|
+
|
|
125
|
+
- `PTM_MCP_READ_ONLY=true` blocks every write tool at call time.
|
|
126
|
+
- `X-PTM-Client` + `X-PTM-MCP-Session` on every outbound request.
|
|
127
|
+
- Env scrub at startup.
|
|
128
|
+
- Startup preflight (`/healthz` + `/auth/me` + `/meta`) with exponential backoff on transient failures and dedicated exit codes per failed layer.
|
|
129
|
+
|
|
130
|
+
Full details: `docs/mcp-security.md`.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
pip install -e packages/ptm-client
|
|
136
|
+
pip install -e 'packages/ptm-mcp[dev]'
|
|
137
|
+
PYTHONPATH=packages/ptm-mcp/src:packages/ptm-client/src \
|
|
138
|
+
pytest packages/ptm-mcp/tests -q
|
|
139
|
+
ruff check packages/ptm-mcp/src packages/ptm-mcp/tests
|
|
140
|
+
ruff format --check packages/ptm-mcp/src packages/ptm-mcp/tests
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
End-to-end smoke (requires a running backend and Node for `npx`):
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
PTM_API_BASE_URL=http://localhost:8010 \
|
|
147
|
+
PTM_API_TOKEN="ptm_u_..." \
|
|
148
|
+
bash scripts/smoke_mcp_inspector.sh
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Exit codes
|
|
152
|
+
|
|
153
|
+
| Code | Meaning |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `0` | clean shutdown |
|
|
156
|
+
| `1` | unhandled exception |
|
|
157
|
+
| `2` | `/healthz` unreachable after 31s of backoff |
|
|
158
|
+
| `3` | `/auth/me` rejected the token |
|
|
159
|
+
| `4` | backend version < 1.9.0 or unparseable |
|
|
160
|
+
| `130` | interrupted (SIGINT) |
|
|
161
|
+
|
|
162
|
+
## See also
|
|
163
|
+
|
|
164
|
+
- `docs/mcp-integration.md` - end-user setup
|
|
165
|
+
- `docs/mcp-developer.md` - adding a tool
|
|
166
|
+
- `docs/mcp-security.md` - tokens, env, headers, read-only, kill switch
|
|
167
|
+
- `docs/mcp-admin.md` - ops runbook + alert response
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ptm-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP stdio server for the Prompt Test Manager API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "Proprietary"}
|
|
12
|
+
authors = [{name = "PTM Team"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: Other/Proprietary License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp>=1.2,<2.0",
|
|
25
|
+
"ptm-client>=0.3.0,<1.0",
|
|
26
|
+
"pydantic>=2.8,<3.0",
|
|
27
|
+
"packaging>=24.0,<26.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.2,<9.0",
|
|
33
|
+
"pytest-asyncio>=0.23,<1.0",
|
|
34
|
+
"pytest-cov>=5.0,<7.0",
|
|
35
|
+
"responses>=0.25,<1.0",
|
|
36
|
+
"ruff>=0.6,<1.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/15five/prompt-test-manager"
|
|
41
|
+
Repository = "https://github.com/15five/prompt-test-manager"
|
|
42
|
+
Changelog = "https://github.com/15five/prompt-test-manager/blob/main/packages/ptm-mcp/CHANGELOG.md"
|
|
43
|
+
Issues = "https://github.com/15five/prompt-test-manager/issues"
|
|
44
|
+
|
|
45
|
+
# Note: the live ``list_providers`` stdio smoke test expects the MCP
|
|
46
|
+
# Inspector CLI to be on PATH. Inspector is a Node package published as
|
|
47
|
+
# ``@modelcontextprotocol/inspector`` (not a PyPI package - the ``mcp-inspector``
|
|
48
|
+
# name on PyPI is an unrelated placeholder). Install globally with
|
|
49
|
+
# ``npm i -g @modelcontextprotocol/inspector`` or let the test invoke it
|
|
50
|
+
# through ``npx``. See scripts/smoke_mcp_inspector.sh.
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
ptm-mcp = "ptm_mcp.__main__:main"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = "py312"
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.format]
|
|
66
|
+
quote-style = "double"
|
|
67
|
+
indent-style = "space"
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
asyncio_mode = "auto"
|
ptm_mcp-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""CLI entry point. Invoked as ``python -m ptm_mcp`` or ``ptm-mcp``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> int:
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .server import run
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
return asyncio.run(run())
|
|
14
|
+
except KeyboardInterrupt:
|
|
15
|
+
return 130
|
|
16
|
+
except Exception as exc: # noqa: BLE001 - top-level boundary
|
|
17
|
+
logging.getLogger("ptm_mcp").exception("fatal: %s", exc)
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Runtime configuration parsed from environment variables.
|
|
2
|
+
|
|
3
|
+
ptm-mcp is a stdio server started by an MCP client (Claude Desktop, Goose,
|
|
4
|
+
etc.) which passes env vars through to the spawned process. We fail fast on
|
|
5
|
+
missing or malformed required values so the client surfaces a clear error
|
|
6
|
+
instead of a cryptic 401 on the first tool call.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
MIN_BACKEND_VERSION = "1.9.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Settings:
|
|
19
|
+
ptm_api_base_url: str
|
|
20
|
+
ptm_api_token: str
|
|
21
|
+
read_only: bool = True
|
|
22
|
+
timeout_seconds: int = 30
|
|
23
|
+
log_level: str = "INFO"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConfigError(RuntimeError):
|
|
27
|
+
"""Raised when required env vars are missing or malformed."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_settings() -> Settings:
|
|
31
|
+
"""Load ptm-mcp settings from environment. Fail fast on missing required vars."""
|
|
32
|
+
base = os.environ.get("PTM_API_BASE_URL", "").strip()
|
|
33
|
+
token = os.environ.get("PTM_API_TOKEN", "").strip()
|
|
34
|
+
if not base:
|
|
35
|
+
raise ConfigError("PTM_API_BASE_URL is required")
|
|
36
|
+
if not token:
|
|
37
|
+
raise ConfigError("PTM_API_TOKEN is required")
|
|
38
|
+
|
|
39
|
+
read_only_env = os.environ.get("PTM_MCP_READ_ONLY", "true").strip().lower()
|
|
40
|
+
read_only = read_only_env not in ("false", "0", "no", "")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
timeout = int(os.environ.get("PTM_MCP_TIMEOUT_SECONDS", "30"))
|
|
44
|
+
except ValueError as exc:
|
|
45
|
+
raise ConfigError("PTM_MCP_TIMEOUT_SECONDS must be an integer") from exc
|
|
46
|
+
if timeout < 1 or timeout > 600:
|
|
47
|
+
raise ConfigError("PTM_MCP_TIMEOUT_SECONDS must be in 1..600")
|
|
48
|
+
|
|
49
|
+
log_level = os.environ.get("PTM_MCP_LOG_LEVEL", "INFO").upper()
|
|
50
|
+
if log_level not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
|
51
|
+
raise ConfigError(f"PTM_MCP_LOG_LEVEL invalid: {log_level}")
|
|
52
|
+
|
|
53
|
+
return Settings(
|
|
54
|
+
ptm_api_base_url=base,
|
|
55
|
+
ptm_api_token=token,
|
|
56
|
+
read_only=read_only,
|
|
57
|
+
timeout_seconds=timeout,
|
|
58
|
+
log_level=log_level,
|
|
59
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Startup env-var scrubber.
|
|
2
|
+
|
|
3
|
+
ptm-mcp inherits the full environment of the MCP client (Claude Desktop,
|
|
4
|
+
Goose, etc.), which typically contains cloud credentials, GitHub tokens,
|
|
5
|
+
and similar secrets unrelated to PTM. The stdio server does not subprocess
|
|
6
|
+
anything, so it has no legitimate use for most of those. We drop everything
|
|
7
|
+
outside a narrow allow-list at startup to shrink the blast radius if the
|
|
8
|
+
process is ever compromised.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("ptm_mcp.env")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# NO PATH in the allow-list. stdio ptm-mcp does not subprocess anything.
|
|
20
|
+
# Adding PATH later would require an opt-in and a documented subprocess caller.
|
|
21
|
+
# See ~/dev/docs/code-review-1.md item C1.
|
|
22
|
+
_ALLOWED_PREFIXES = ("PTM_", "LC_")
|
|
23
|
+
_ALLOWED_EXACT = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"HOME",
|
|
26
|
+
"USER",
|
|
27
|
+
"LANG",
|
|
28
|
+
"TZ",
|
|
29
|
+
"TERM",
|
|
30
|
+
"PYTHONPATH",
|
|
31
|
+
"PYTHONUNBUFFERED",
|
|
32
|
+
"SSL_CERT_FILE",
|
|
33
|
+
"REQUESTS_CA_BUNDLE",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def scrub_env() -> int:
|
|
39
|
+
"""Remove env vars outside the allow-list. Returns count dropped."""
|
|
40
|
+
dropped: list[str] = []
|
|
41
|
+
for key in list(os.environ):
|
|
42
|
+
if key in _ALLOWED_EXACT:
|
|
43
|
+
continue
|
|
44
|
+
if any(key.startswith(p) for p in _ALLOWED_PREFIXES):
|
|
45
|
+
continue
|
|
46
|
+
dropped.append(key)
|
|
47
|
+
del os.environ[key]
|
|
48
|
+
if dropped:
|
|
49
|
+
logger.info(
|
|
50
|
+
"[ptm-mcp] Scrubbed %d env vars at startup. Kept %d.",
|
|
51
|
+
len(dropped),
|
|
52
|
+
len(os.environ),
|
|
53
|
+
)
|
|
54
|
+
return len(dropped)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""MCP chokepoint signal headers.
|
|
2
|
+
|
|
3
|
+
The PTM backend middleware (``@app.middleware("http")``) reads
|
|
4
|
+
``X-PTM-Client`` to classify incoming requests as MCP-tagged and
|
|
5
|
+
``X-PTM-MCP-Session`` to correlate the whole agent conversation with
|
|
6
|
+
rows in ``mcp_usage`` and ``RunRecord.mcp_session_id``. See
|
|
7
|
+
``docs/mcp-ptm-phase1.md`` sections 18.1 and 18.6.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def new_session_id() -> str:
|
|
18
|
+
"""UUID for the lifetime of one ptm-mcp server process.
|
|
19
|
+
|
|
20
|
+
Attached to every outbound HTTP call as ``X-PTM-MCP-Session``. The
|
|
21
|
+
backend correlates this with ``mcp_usage`` and ``RunRecord.mcp_session_id``
|
|
22
|
+
for forensic replay of an agent conversation.
|
|
23
|
+
"""
|
|
24
|
+
return str(uuid4())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_client_headers(session_id: str) -> dict[str, str]:
|
|
28
|
+
"""Return the MCP-chokepoint signal headers the backend middleware reads."""
|
|
29
|
+
return {
|
|
30
|
+
"X-PTM-Client": f"ptm-mcp/{__version__}",
|
|
31
|
+
"X-PTM-MCP-Session": session_id,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def install_client_headers(client, session_id: str) -> None:
|
|
36
|
+
"""Merge MCP headers onto an existing ``PTMClient``.
|
|
37
|
+
|
|
38
|
+
v0.3.0 exposes ``client._headers`` as a plain dict; mutating it is the
|
|
39
|
+
simplest path that does not fork the client. If a future release hides
|
|
40
|
+
the dict, this helper becomes the single place to adapt.
|
|
41
|
+
"""
|
|
42
|
+
headers = getattr(client, "_headers", None)
|
|
43
|
+
if headers is None or not isinstance(headers, dict):
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
"PTMClient does not expose a mutable _headers dict; update ptm-mcp to "
|
|
46
|
+
"match the current ptm-client shape."
|
|
47
|
+
)
|
|
48
|
+
headers.update(build_client_headers(session_id))
|