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 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
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """ptm-mcp: MCP stdio server for the Prompt Test Manager API."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -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))