@strixgov/mcp-proxy 0.1.0 → 0.1.5
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.
- package/CHANGELOG.md +227 -2
- package/README.md +224 -17
- package/SECURITY.md +6 -6
- package/examples/notion-proxy-config.json +1 -0
- package/package.json +2 -4
- package/src/config.mjs +26 -1
- package/src/proxy.mjs +175 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,232 @@ All notable changes to `@strixgov/mcp-proxy` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.5]
|
|
9
|
+
|
|
10
|
+
Documentation-hygiene patch. No functional change to the proxy, the
|
|
11
|
+
receipt format, the signing flow, or any runtime behavior.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **`SECURITY.md`, `README.md`, and `CHANGELOG.md` cross-references**
|
|
15
|
+
rewritten to remove links into the maintainer's internal source tree.
|
|
16
|
+
The threat-model cross-reference in `SECURITY.md` now points reviewers
|
|
17
|
+
at `security@strixgov.com`; runbook cross-references and an internal
|
|
18
|
+
development-branch name embedded in a 0.1.0 release note have been
|
|
19
|
+
removed. Static text only — no API, no behavior, no tests changed.
|
|
20
|
+
|
|
21
|
+
### Note
|
|
22
|
+
- Operators on 0.1.0 are encouraged to upgrade. v0.1.0 has been
|
|
23
|
+
deprecated on npm to surface this guidance on install; `npm install
|
|
24
|
+
@strixgov/mcp-proxy@0.1.0` continues to work but emits a deprecation
|
|
25
|
+
warning pointing here.
|
|
26
|
+
|
|
27
|
+
## [0.1.4]
|
|
28
|
+
|
|
29
|
+
Fixes the silent-ephemeral-key footgun surfaced by the same dogfood that
|
|
30
|
+
produced 0.1.1–0.1.3. With `storagePath` unset, pre-0.1.4 left
|
|
31
|
+
`governMCPServer` to default both halves of persistence wrong:
|
|
32
|
+
`MemoryStorage` (so the proxy's receipts vanished on exit) AND
|
|
33
|
+
`generateSigningKey()` ephemeral (so the kid embedded in every on-disk
|
|
34
|
+
receipt pointed at a key that didn't exist anywhere). Operators who
|
|
35
|
+
worked around the storage half by pointing `storagePath` at
|
|
36
|
+
`~/.strix-gateway` saw receipts they could `strix-gateway receipts list`
|
|
37
|
+
but never `verify` — `kid not in keyring: strix-<serverId>` on every
|
|
38
|
+
record. This release makes "the receipts I wrote yesterday are
|
|
39
|
+
verifiable today" the default.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **Default `storagePath`** when `opts.storagePath` is undefined: now
|
|
43
|
+
resolves to `path.join(os.homedir(), ".strix-mcp-proxy", serverId)`
|
|
44
|
+
instead of leaving the value unset. Receipts persist as JSONL under
|
|
45
|
+
that path; the Ed25519 signing key persists at
|
|
46
|
+
`<storagePath>/keys/{signing-key.pem, public-jwk.json}` and is reused
|
|
47
|
+
across restarts. The default is intentionally **not**
|
|
48
|
+
`~/.strix-gateway`: the gateway CLI uses a multi-kid keyring layout
|
|
49
|
+
(`active` pointer + `<kid>/` subdirs) while the proxy uses
|
|
50
|
+
`loadOrCreateSigningKey`'s flat single-key layout, and sharing the
|
|
51
|
+
same `keys/` directory produces silently-broken receipts (the proxy's
|
|
52
|
+
flat key files are invisible to the keyring loader, or the v0.1 auto-
|
|
53
|
+
migration clobbers the gateway's `active` pointer to the proxy's kid).
|
|
54
|
+
- **Audit event `signing.key.resolved`** now distinguishes
|
|
55
|
+
`source: "persistent"` (operator-supplied `storagePath`) from
|
|
56
|
+
`source: "persistent-default"` (auto-resolved per-serverId default).
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- **`storagePath: null` opt-out.** Callers who genuinely want the pre-
|
|
60
|
+
0.1.4 ephemeral behavior (CI smoke runs, demos that intentionally
|
|
61
|
+
produce throwaway receipts) pass `storagePath: null` explicitly.
|
|
62
|
+
Receipts within the session remain chain-coherent; signatures are
|
|
63
|
+
unverifiable across restarts.
|
|
64
|
+
|
|
65
|
+
### Test
|
|
66
|
+
- `startTestProxy` helper now defaults to a per-test tmpdir so the
|
|
67
|
+
suite never writes to the developer's home directory.
|
|
68
|
+
- Old `"no storagePath ⇒ in-memory ephemeral key (preserves pre-0.1.2
|
|
69
|
+
default)"` test removed and replaced with two tests pinning the new
|
|
70
|
+
default behavior + the explicit-`null` opt-out path.
|
|
71
|
+
|
|
72
|
+
### Migration notes for existing operators
|
|
73
|
+
- If your `notion-proxy-config.json` (or equivalent) sets
|
|
74
|
+
`"storagePath": "~/.strix-gateway"` to share storage with the
|
|
75
|
+
gateway CLI, **change it** — point at
|
|
76
|
+
`"~/.strix-mcp-proxy/<serverId>"` instead (or any path you control
|
|
77
|
+
that isn't aliased to `~/.strix-gateway`). Existing receipts at the
|
|
78
|
+
old location remain readable but their kid is unverifiable because
|
|
79
|
+
the proxy's old ephemeral key is gone. Future receipts written
|
|
80
|
+
under the new default ARE verifiable end-to-end.
|
|
81
|
+
- If your code passes no `storagePath` and depended on receipts
|
|
82
|
+
vanishing on exit, add `storagePath: null` to your `startProxy`
|
|
83
|
+
call to preserve that behavior.
|
|
84
|
+
|
|
85
|
+
## [0.1.3]
|
|
86
|
+
|
|
87
|
+
Closes the headless-approval gap surfaced by the same dogfood run that
|
|
88
|
+
produced 0.1.1 / 0.1.2. With `approval.enabled: false` (the default)
|
|
89
|
+
every `APPROVAL_REQUIRED` call is silently denied; turning approval on
|
|
90
|
+
defaults to `terminalApprove`, which fails closed in any non-TTY context
|
|
91
|
+
including Claude Desktop's headless spawn. `@strixgov/tool-gateway`
|
|
92
|
+
already shipped a `fileApprover` that works in headless contexts, but
|
|
93
|
+
the proxy couldn't wire it up from JSON config because the gateway's
|
|
94
|
+
`approval.prompt` field expects a function — and JSON can't carry one.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
- **`approval.type` JSON config field** with three accepted values:
|
|
98
|
+
- **`"auto"`** — auto-approves every `APPROVAL_REQUIRED` call with a
|
|
99
|
+
signed `approval.approved: true` audit trail. Convenient for demos
|
|
100
|
+
where you want to show the approval-loop fires; **defeats real
|
|
101
|
+
governance** for non-trivial environments.
|
|
102
|
+
- **`"file"`** — wires up `fileApprover` from `@strixgov/tool-gateway`.
|
|
103
|
+
The proxy writes `<requestDir>/<requestId>.request.json` per
|
|
104
|
+
approval-pending call; an out-of-band approver writes
|
|
105
|
+
`<requestId>.response.json` with `{approved: true|false, approvedBy?, reason?}`.
|
|
106
|
+
Works under Claude Desktop spawn, CI, or any headless context.
|
|
107
|
+
`requestDir` defaults to `<storagePath>/approvals` when `storagePath`
|
|
108
|
+
is set, or `./.strix-approvals` otherwise. `~` and `~/` are expanded
|
|
109
|
+
to `os.homedir()` consistent with `storagePath`'s tilde handling.
|
|
110
|
+
Optional `timeoutMs` (default 5min) and `pollIntervalMs` (default
|
|
111
|
+
250ms) tunables.
|
|
112
|
+
- **`"terminal"`** — explicit alias for the legacy default. Useful
|
|
113
|
+
when an operator is testing the proxy directly from a TTY and wants
|
|
114
|
+
to see the y/N prompt.
|
|
115
|
+
- Resolution order pinned and tested:
|
|
116
|
+
1. Programmatic `approval.prompt` function (always wins)
|
|
117
|
+
2. `approval.enabled === false`
|
|
118
|
+
3. `approval.autoApprove === true` (long form of `type: "auto"`)
|
|
119
|
+
4. `approval.type === "auto" | "file" | "terminal"`
|
|
120
|
+
5. Unknown `type` ⇒ startup error (fail-fast, not silent fail-closed)
|
|
121
|
+
- 8 new tests covering each resolution branch.
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
- Unknown `approval.type` values now throw at startup with the message
|
|
125
|
+
`unknown approval.type 'X' — expected "terminal", "file", or "auto"`,
|
|
126
|
+
instead of silently falling through to a deny-everything state.
|
|
127
|
+
|
|
128
|
+
### Migration
|
|
129
|
+
- **`{enabled: false}`** (the docs-example default) is unchanged.
|
|
130
|
+
- **`{enabled: true}`** (no `type`) preserves the v0.1.2 behavior
|
|
131
|
+
(defaults to `terminalApprove`, fails closed in non-TTY contexts).
|
|
132
|
+
- New JSON-config-friendly shapes for headless deployments:
|
|
133
|
+
```jsonc
|
|
134
|
+
"approval": {
|
|
135
|
+
"enabled": true,
|
|
136
|
+
"type": "file",
|
|
137
|
+
"requestDir": "~/.strix-gateway/approvals",
|
|
138
|
+
"timeoutMs": 300000
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
or for demos:
|
|
142
|
+
```jsonc
|
|
143
|
+
"approval": { "enabled": true, "type": "auto" }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Verification
|
|
147
|
+
- 37 tests pass under `node --test`; 1 skipped (network-gated tarball
|
|
148
|
+
install gate, opt in via `STRIX_RUN_TARBALL_INSTALL=1`).
|
|
149
|
+
|
|
150
|
+
## [0.1.2]
|
|
151
|
+
|
|
152
|
+
Closes the verifiability gap surfaced by a fresh dogfood run on Windows
|
|
153
|
+
(May 25 2026): receipts were correctly signed and written to JSONL on
|
|
154
|
+
disk, but the signing key was discarded the instant the proxy exited.
|
|
155
|
+
Every receipt produced under v0.1.0 / v0.1.1 carried a `signingKeyId`
|
|
156
|
+
(e.g. `strix-notion`) pointing at a key that no longer existed
|
|
157
|
+
anywhere — so `@strixgov/verifier` reported `KEY_NOT_FOUND` for every
|
|
158
|
+
local-mode receipt. Trust property was "tamper-evident at write time"
|
|
159
|
+
but not "externally re-verifiable after restart."
|
|
160
|
+
|
|
161
|
+
### Fixed
|
|
162
|
+
- **`startProxy` now persists the signing keypair to disk when
|
|
163
|
+
`storagePath` is set.** Files land at:
|
|
164
|
+
- `<storagePath>/keys/signing-key.pem` (PKCS8 PEM, mode 0600)
|
|
165
|
+
- `<storagePath>/keys/public-jwk.json` (public JWK with `kid`, `kty`,
|
|
166
|
+
`crv`, `x` — mode 0644)
|
|
167
|
+
The same `kid` (e.g. `strix-notion`) is reused across restarts via
|
|
168
|
+
`loadOrCreateSigningKey()` from `@strixgov/tool-gateway`, so JSONL
|
|
169
|
+
receipts are independently verifiable after the proxy exits:
|
|
170
|
+
```sh
|
|
171
|
+
npx @strixgov/verifier chain ~/.strix-gateway/receipts.jsonl \
|
|
172
|
+
--jwks ~/.strix-gateway/keys/public-jwk.json
|
|
173
|
+
```
|
|
174
|
+
Caller-supplied `opts.signingKey` still wins over disk persistence —
|
|
175
|
+
operators integrating with a KMS or secrets manager are unaffected.
|
|
176
|
+
When neither `signingKey` nor `storagePath` is set, behavior matches
|
|
177
|
+
v0.1.1 (in-memory ephemeral key) — preserved as the default for tests
|
|
178
|
+
and embedded use cases.
|
|
179
|
+
- A new `signing.key.resolved` audit event fires on the persistent
|
|
180
|
+
path with `{source: "persistent", kid, keyPath}` so operators can
|
|
181
|
+
confirm the disk-load happened.
|
|
182
|
+
|
|
183
|
+
### Breaking changes
|
|
184
|
+
- None. `startProxy` is already async; the new `loadOrCreateSigningKey`
|
|
185
|
+
call slots into the existing async flow without changing the
|
|
186
|
+
`governMCPServer` (synchronous) contract.
|
|
187
|
+
|
|
188
|
+
### Notes for existing users
|
|
189
|
+
- **Receipts produced under v0.1.0 / v0.1.1 remain unverifiable.** Their
|
|
190
|
+
signing keys were ephemeral and are not recoverable. Starting with
|
|
191
|
+
0.1.2, every new receipt written under a `storagePath` deployment is
|
|
192
|
+
verifiable for the life of the persisted key.
|
|
193
|
+
- The first start under 0.1.2 generates a fresh keypair. Subsequent
|
|
194
|
+
starts reuse it. To force key rotation, delete `<storagePath>/keys/`
|
|
195
|
+
before restart; the next start will regenerate.
|
|
196
|
+
|
|
197
|
+
### Verification
|
|
198
|
+
- 4 new tests in `test/proxy-end-to-end.test.mjs`:
|
|
199
|
+
- Disk files (PEM + JWK) created with correct shape + kid
|
|
200
|
+
- kid survives a restart (the demo-story invariant)
|
|
201
|
+
- Caller-supplied `signingKey` bypasses disk persistence
|
|
202
|
+
- No-`storagePath` path preserves v0.1.1 in-memory default
|
|
203
|
+
|
|
204
|
+
## [0.1.1]
|
|
205
|
+
|
|
206
|
+
Bug-fix release that closes the gap surfaced when a fresh user ran the
|
|
207
|
+
"5-line integration (Claude Desktop)" path end-to-end and the proxy
|
|
208
|
+
exited at startup with `Cannot find package '@strixgov/capabilities-mcp-common'`.
|
|
209
|
+
|
|
210
|
+
### Fixed
|
|
211
|
+
- **`@strixgov/capabilities-mcp-common` is now a runtime `dependencies`
|
|
212
|
+
entry, not a `devDependencies`-only listing.** Previously, every user
|
|
213
|
+
who installed the proxy globally (`npm install -g @strixgov/mcp-proxy`)
|
|
214
|
+
also had to discover and run a second `npm install -g
|
|
215
|
+
@strixgov/capabilities-mcp-common` before the example config in the
|
|
216
|
+
README would load — because npm doesn't install devDependencies for
|
|
217
|
+
globally-installed packages. The README documents the companion pack
|
|
218
|
+
as the default capability source, so it's a hard runtime dependency.
|
|
219
|
+
This was a release blocker for the "zero-friction first run" claim
|
|
220
|
+
in the README.
|
|
221
|
+
- **Clearer error when a `capabilities` import path can't be resolved.**
|
|
222
|
+
`resolveCapabilities()` now catches `ERR_MODULE_NOT_FOUND` (and the
|
|
223
|
+
related `Cannot find package` shape) and re-throws with an actionable
|
|
224
|
+
hint pointing the operator at `npm install -g <pack>`. Previously
|
|
225
|
+
the operator saw the bare Node loader error, which doesn't tell them
|
|
226
|
+
how to fix it.
|
|
227
|
+
|
|
228
|
+
### Verification
|
|
229
|
+
- All 25 tests under `node --test` continue to pass.
|
|
230
|
+
- Manual dogfood: `npm install -g @strixgov/mcp-proxy@0.1.1` followed
|
|
231
|
+
by `strix-mcp-proxy --config <path>` resolves
|
|
232
|
+
`@strixgov/capabilities-mcp-common/notion` without a second install.
|
|
233
|
+
|
|
8
234
|
## [0.1.0]
|
|
9
235
|
|
|
10
236
|
Initial release. The Strix MCP Proxy — a standalone process that
|
|
@@ -64,8 +290,7 @@ rather than a v1.0 rewrite.
|
|
|
64
290
|
expansion, companion-pack dynamic import resolution.
|
|
65
291
|
- `test/cli.test.mjs` — `--help`, `--version`, missing-arg errors,
|
|
66
292
|
missing-file errors.
|
|
67
|
-
- Companion-pack lookup uses `cap.name` indexing
|
|
68
|
-
shipped on `claude/strix-mcp-wrapping-IDXOy`) so server-prefixed
|
|
293
|
+
- Companion-pack lookup uses `cap.name` indexing so server-prefixed
|
|
69
294
|
tool names (`notion-fetch`, `slack_send_message`) match the pack
|
|
70
295
|
rather than falling through to the MEDIUM EXECUTE heuristic.
|
|
71
296
|
|
package/README.md
CHANGED
|
@@ -35,22 +35,44 @@ Tool implementations are untouched. The MCP client (Claude Desktop, mcp-cli,
|
|
|
35
35
|
IDE integration, your own code) doesn't need to know Strix exists — it just
|
|
36
36
|
spawns this proxy in place of the upstream server.
|
|
37
37
|
|
|
38
|
+
> **Transport scope (v0.1.0):** the proxy wraps MCP servers that speak
|
|
39
|
+
> the **stdio transport** — the `command` / `args` pattern in
|
|
40
|
+
> `claude_desktop_config.json`. It does **not** intercept HTTP-based
|
|
41
|
+
> hosted MCP servers (e.g. Notion's `https://mcp.notion.com/mcp`
|
|
42
|
+
> Claude Desktop "Connector" pattern). If your MCP server is reachable
|
|
43
|
+
> only over HTTPS, the proxy cannot govern it today; that's
|
|
44
|
+
> [Posture B](https://github.com/Strixgov/strix/tree/main/docs/architecture/mcp-mode-3-enforcement-v1.md)
|
|
45
|
+
> in the Mode 3 architecture and is deferred to a future release. Use
|
|
46
|
+
> the local stdio version of the upstream MCP server in the meantime.
|
|
47
|
+
|
|
38
48
|
## 5-line integration (Claude Desktop)
|
|
39
49
|
|
|
50
|
+
Claude Desktop's config file lives at:
|
|
51
|
+
|
|
52
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
53
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` (resolves to `C:\Users\<you>\AppData\Roaming\Claude\claude_desktop_config.json` — outside any OneDrive Known Folder Move)
|
|
54
|
+
- **Linux:** `~/.config/Claude/claude_desktop_config.json`
|
|
55
|
+
|
|
40
56
|
```jsonc
|
|
41
|
-
// ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
42
57
|
{
|
|
43
58
|
"mcpServers": {
|
|
44
59
|
"notion": {
|
|
45
60
|
"command": "strix-mcp-proxy",
|
|
46
|
-
"args": ["--config", "/
|
|
61
|
+
"args": ["--config", "/absolute/path/to/notion-proxy-config.json"],
|
|
62
|
+
// Env vars set here propagate to the proxy and on to the upstream
|
|
63
|
+
// MCP server via process.env (the proxy passes them through by
|
|
64
|
+
// default — see `upstream.env` below to override explicitly).
|
|
65
|
+
"env": {
|
|
66
|
+
"NOTION_TOKEN": "secret_..."
|
|
67
|
+
}
|
|
47
68
|
}
|
|
48
69
|
}
|
|
49
70
|
}
|
|
50
71
|
```
|
|
51
72
|
|
|
52
|
-
The config file at `notion.json` points the proxy at the
|
|
53
|
-
server and sets your policy. See
|
|
73
|
+
The config file at `notion-proxy-config.json` points the proxy at the
|
|
74
|
+
real Notion MCP server and sets your policy. See
|
|
75
|
+
[`examples/notion-proxy-config.json`](./examples/notion-proxy-config.json)
|
|
54
76
|
for a complete example.
|
|
55
77
|
|
|
56
78
|
## Config file shape
|
|
@@ -60,7 +82,14 @@ for a complete example.
|
|
|
60
82
|
"serverId": "notion", // namespace for capability ids
|
|
61
83
|
"upstream": {
|
|
62
84
|
"command": "npx",
|
|
63
|
-
"args": ["-y", "@notionhq/notion-mcp-server"]
|
|
85
|
+
"args": ["-y", "@notionhq/notion-mcp-server"],
|
|
86
|
+
// Optional. If omitted, the proxy passes process.env through to
|
|
87
|
+
// the upstream (so env vars set in Claude Desktop's `env` block
|
|
88
|
+
// reach the upstream automatically). Setting `upstream.env`
|
|
89
|
+
// overrides that default — only the keys you list are passed.
|
|
90
|
+
"env": {
|
|
91
|
+
"NOTION_TOKEN": "secret_..."
|
|
92
|
+
}
|
|
64
93
|
},
|
|
65
94
|
"capabilities": "@strixgov/capabilities-mcp-common/notion", // import path
|
|
66
95
|
"policy": {
|
|
@@ -75,8 +104,13 @@ for a complete example.
|
|
|
75
104
|
},
|
|
76
105
|
"default": "DENY"
|
|
77
106
|
},
|
|
78
|
-
"approval": {
|
|
79
|
-
|
|
107
|
+
"approval": {
|
|
108
|
+
"enabled": true,
|
|
109
|
+
"type": "file", // headless-friendly approver (see "Approval modes" below)
|
|
110
|
+
"requestDir": "~/.strix-mcp-proxy/notion/approvals",
|
|
111
|
+
"timeoutMs": 300000 // 5 minutes
|
|
112
|
+
},
|
|
113
|
+
"storagePath": "~/.strix-mcp-proxy/notion" // optional (this IS the default for serverId="notion"); set to `null` for ephemeral
|
|
80
114
|
}
|
|
81
115
|
```
|
|
82
116
|
|
|
@@ -85,6 +119,85 @@ for a complete example.
|
|
|
85
119
|
`@strixgov/capabilities-mcp-common/github`, etc.) — resolved at startup
|
|
86
120
|
- an inline array of `McpCapability` objects (caller-managed classification)
|
|
87
121
|
|
|
122
|
+
### Where the upstream's secrets should live
|
|
123
|
+
|
|
124
|
+
v0.1.0 does **no env-var substitution inside the config file** — there
|
|
125
|
+
is no `${NOTION_TOKEN}` template syntax. Two supported patterns for
|
|
126
|
+
upstream secrets:
|
|
127
|
+
|
|
128
|
+
1. **Set in Claude Desktop's `env` block** (recommended for
|
|
129
|
+
first-time setup) — the MCP client passes them to the proxy
|
|
130
|
+
process; the proxy passes `process.env` to the upstream by
|
|
131
|
+
default. Token never appears in `notion-proxy-config.json`, so
|
|
132
|
+
the config file is safe to check into source control (without the
|
|
133
|
+
policy values that depend on environment, of course).
|
|
134
|
+
2. **Set in `upstream.env`** (recommended when the proxy is the
|
|
135
|
+
only consumer of the upstream) — explicit, narrower exposure
|
|
136
|
+
surface. Only the keys you list reach the upstream; everything
|
|
137
|
+
else from `process.env` is filtered out.
|
|
138
|
+
|
|
139
|
+
On Windows specifically, prefer placing `storagePath` outside any
|
|
140
|
+
OneDrive Known Folder Move directory. The default `~/.strix-mcp-proxy/`
|
|
141
|
+
resolves to `C:\Users\<you>\.strix-mcp-proxy\` (a dotfile-style
|
|
142
|
+
directory under the user root), which is **not** covered by Known
|
|
143
|
+
Folder Move and stays local.
|
|
144
|
+
|
|
145
|
+
## Approval modes
|
|
146
|
+
|
|
147
|
+
The proxy's `approval` config field accepts a `type` string that maps to
|
|
148
|
+
the appropriate approver from `@strixgov/tool-gateway`. The gateway's
|
|
149
|
+
underlying `approval.prompt` field expects a function, which JSON config
|
|
150
|
+
can't carry — `type` is the JSON-friendly bridge.
|
|
151
|
+
|
|
152
|
+
| `approval.type` | What it does | When to use it |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| omitted, `approval.enabled: false` | No prompt — every `APPROVAL_REQUIRED` decision becomes `DENY` with `reason: "PROMPT_FAILED"`. | Default; safest if you haven't built an approval pathway yet. |
|
|
155
|
+
| `"auto"` | Auto-approves every call that hits the gate. Receipt records `approval.approved: true, approvedBy: "auto"`. | Demos; smoke tests; environments where you want to show the approval-loop wired up without a real human in the loop. **Defeats meaningful governance** for production. |
|
|
156
|
+
| `"file"` | Wires up `fileApprover`: proxy writes `<requestDir>/<requestId>.request.json`; an out-of-band approver writes `<requestId>.response.json` with `{approved, approvedBy?, reason?}`. | Production-leaning headless deployments — Claude Desktop, CI, containerised agents. Pair with whatever notification channel you want (Slack bot, GitHub Actions check, on-call rotation script) that ultimately writes the response file. |
|
|
157
|
+
| `"terminal"` | Interactive `y/N` prompt on stdin. Fails closed (`PROMPT_FAILED → DENY`) in any non-TTY context. | Direct-CLI testing where you're running the proxy yourself and want to manually approve. **Not viable under Claude Desktop spawn** — Claude Desktop owns the stdio channel; there's no terminal to prompt to. |
|
|
158
|
+
|
|
159
|
+
**Example: file-based approval under Claude Desktop.** In your
|
|
160
|
+
`notion-proxy-config.json`:
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
"approval": {
|
|
164
|
+
"enabled": true,
|
|
165
|
+
"type": "file",
|
|
166
|
+
"requestDir": "~/.strix-gateway/approvals",
|
|
167
|
+
"timeoutMs": 300000 // 5 minutes
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Then write a tiny watcher that approves or denies. Example PowerShell
|
|
172
|
+
sketch (one approval at a time):
|
|
173
|
+
|
|
174
|
+
```powershell
|
|
175
|
+
$watchDir = "$env:USERPROFILE\.strix-gateway\approvals"
|
|
176
|
+
while ($true) {
|
|
177
|
+
Get-ChildItem $watchDir -Filter "*.request.json" | ForEach-Object {
|
|
178
|
+
$req = Get-Content $_.FullName | ConvertFrom-Json
|
|
179
|
+
Write-Host "PENDING: $($req.invocation.capabilityId) actor=$($req.invocation.actorId)"
|
|
180
|
+
$answer = Read-Host "Approve? [y/N]"
|
|
181
|
+
$approved = $answer -in @("y","Y","yes")
|
|
182
|
+
$respPath = $_.FullName -replace ".request.json$", ".response.json"
|
|
183
|
+
@{ approved = $approved; approvedBy = $env:USERNAME } | ConvertTo-Json | Set-Content $respPath
|
|
184
|
+
}
|
|
185
|
+
Start-Sleep -Milliseconds 500
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Operator + audit channels (Slack, PagerDuty, ServiceNow, etc.) replace
|
|
190
|
+
the `Read-Host` line — same file-shape on both ends.
|
|
191
|
+
|
|
192
|
+
Resolution order if multiple approval fields are set (caller-supplied
|
|
193
|
+
programmatic config wins):
|
|
194
|
+
|
|
195
|
+
1. `approval.prompt` is a function ⇒ pass through (caller intent wins)
|
|
196
|
+
2. `approval.enabled === false` ⇒ pass through (gate disabled)
|
|
197
|
+
3. `approval.autoApprove === true` ⇒ pass through (long form of `type: "auto"`)
|
|
198
|
+
4. `approval.type ∈ {"auto","file","terminal"}` ⇒ resolved
|
|
199
|
+
5. Unknown `type` ⇒ startup error (fail-fast)
|
|
200
|
+
|
|
88
201
|
## CLI flags (no config file needed for simple cases)
|
|
89
202
|
|
|
90
203
|
```sh
|
|
@@ -115,7 +228,7 @@ Run `strix-mcp-proxy --help` for the full flag list.
|
|
|
115
228
|
| Per-capability rules + risk overrides | ✓ | — |
|
|
116
229
|
| Approval gate (any approver implementation) | ✓ | — |
|
|
117
230
|
| Local-mode signed receipts (in-memory or JSONL) | ✓ | — |
|
|
118
|
-
| Connected mode (sync receipts to `strixgov.com`) | ✓ |
|
|
231
|
+
| Connected mode (sync receipts to `strixgov.com`) | ✓ programmatic only via `startProxy({ connectedMode })` | ✓ JSON config |
|
|
119
232
|
| Mode 1 (Observed) | ✓ | — |
|
|
120
233
|
| Mode 2 (Approval-Gated) | ✓ | — |
|
|
121
234
|
| **Mode 3 (Capability-Enforced) — `execution_authorization_v1` token mint + transport** | — | ✓ |
|
|
@@ -144,7 +257,11 @@ The cryptographic primitive is already shipped — see
|
|
|
144
257
|
v0.1.0 of the proxy does not mint these tokens yet; what it does is
|
|
145
258
|
make sure the *seam* exists so v0.2.0 plugs in cleanly.
|
|
146
259
|
|
|
147
|
-
## Listening to receipts
|
|
260
|
+
## Listening to receipts (programmatic embedding)
|
|
261
|
+
|
|
262
|
+
For embedded use cases (your own MCP host, custom orchestration, etc.)
|
|
263
|
+
where you want a callback on every signed decision rather than
|
|
264
|
+
polling the JSONL file:
|
|
148
265
|
|
|
149
266
|
```js
|
|
150
267
|
import { startProxy } from "@strixgov/mcp-proxy";
|
|
@@ -160,14 +277,77 @@ const handle = await startProxy({
|
|
|
160
277
|
});
|
|
161
278
|
```
|
|
162
279
|
|
|
163
|
-
|
|
280
|
+
For verifying those receipts independently, see the next section.
|
|
281
|
+
|
|
282
|
+
## Verifying receipts
|
|
283
|
+
|
|
284
|
+
Every receipt the proxy emits is byte-compatible with
|
|
285
|
+
[`@strixgov/verifier`](https://www.npmjs.com/package/@strixgov/verifier)
|
|
286
|
+
— a zero-Strix-dependency package. The verifier needs two inputs: the
|
|
287
|
+
receipt JSONL file AND the public JWK that signed those receipts. How
|
|
288
|
+
you get the JWK depends on whether you set `storagePath` in your config.
|
|
289
|
+
|
|
290
|
+
### Default (v0.1.4+)
|
|
291
|
+
|
|
292
|
+
As of v0.1.4, **`storagePath` defaults to `~/.strix-mcp-proxy/<serverId>`
|
|
293
|
+
when unset**. Receipts persist as JSONL under that path; the Ed25519
|
|
294
|
+
signing key persists at `<storagePath>/keys/{signing-key.pem,
|
|
295
|
+
public-jwk.json}` on first start (PKCS8 PEM at `0600` / public JWK at
|
|
296
|
+
`0644`) and is reused across restarts. The default partitions per
|
|
297
|
+
`serverId` so multiple wrapped upstreams (notion + github + filesystem,
|
|
298
|
+
say) each get their own receipts file and signing key.
|
|
299
|
+
|
|
300
|
+
The verification command is one line:
|
|
164
301
|
|
|
165
302
|
```sh
|
|
166
|
-
|
|
303
|
+
# macOS / Linux
|
|
304
|
+
npx @strixgov/verifier chain ~/.strix-mcp-proxy/notion/receipts.jsonl \
|
|
305
|
+
--jwks ~/.strix-mcp-proxy/notion/keys/public-jwk.json
|
|
306
|
+
|
|
307
|
+
# Windows PowerShell
|
|
308
|
+
npx '@strixgov/verifier' chain "$env:USERPROFILE\.strix-mcp-proxy\notion\receipts.jsonl" `
|
|
309
|
+
--jwks "$env:USERPROFILE\.strix-mcp-proxy\notion\keys\public-jwk.json"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The verifier resolves `signingKeyId` against the JWKS, recomputes the
|
|
313
|
+
canonical bytes, verifies the Ed25519 signature, walks the proof chain,
|
|
314
|
+
and prints `N/N receipts VERIFIED`. The verifier uses only `node:crypto`;
|
|
315
|
+
no Strix-operated service is in the trust path.
|
|
316
|
+
|
|
317
|
+
> **Do not alias `storagePath` to `~/.strix-gateway`.** That directory
|
|
318
|
+
> is owned by the `strix-gateway` CLI's keyring, which uses a multi-kid
|
|
319
|
+
> layout (`active` pointer file + `<kid>/` subdirectories). The proxy
|
|
320
|
+
> writes a flat single-key layout (`signing-key.pem` + `public-jwk.json`
|
|
321
|
+
> at the keys/ root) — sharing the same `keys/` dir collides silently:
|
|
322
|
+
> the proxy's flat key files are invisible to the keyring loader, or
|
|
323
|
+
> the v0.1 auto-migration clobbers the gateway's active pointer to the
|
|
324
|
+
> proxy's kid. The default keeps these stores cleanly separated.
|
|
325
|
+
|
|
326
|
+
### Explicit ephemeral mode (`storagePath: null`)
|
|
327
|
+
|
|
328
|
+
If you want the pre-v0.1.4 behavior — fresh Ed25519 keypair in memory
|
|
329
|
+
at every startup, never persisted — pass `storagePath: null` explicitly
|
|
330
|
+
to `startProxy`. Receipts within a single proxy session remain chain-
|
|
331
|
+
coherent (each receipt links to the previous one's `proofChainHash`),
|
|
332
|
+
but signatures cannot be verified across restarts.
|
|
333
|
+
|
|
334
|
+
For a single-session demo, extract the JWK via the programmatic
|
|
335
|
+
`startProxy` handle before the proxy exits:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
import { startProxy } from "@strixgov/mcp-proxy";
|
|
339
|
+
import fs from "node:fs/promises";
|
|
340
|
+
|
|
341
|
+
const handle = await startProxy({ storagePath: null, /* …your config… */ });
|
|
342
|
+
await fs.writeFile(
|
|
343
|
+
"./public-jwks.json",
|
|
344
|
+
JSON.stringify({ keys: [handle.signingKey.publicKeyJwk] }, null, 2),
|
|
345
|
+
);
|
|
167
346
|
```
|
|
168
347
|
|
|
169
|
-
|
|
170
|
-
|
|
348
|
+
Then point the verifier at that JWKS file. **For any operational use,
|
|
349
|
+
let `storagePath` default (or set it explicitly) instead** — persistent
|
|
350
|
+
keys are strictly better in every dimension that isn't a one-off demo.
|
|
171
351
|
|
|
172
352
|
## What this proxy is NOT
|
|
173
353
|
|
|
@@ -177,9 +357,39 @@ trust path of receipt verification.
|
|
|
177
357
|
the library; the proxy is the standalone process. They share the
|
|
178
358
|
same governance core (`@strixgov/tool-gateway`) and produce
|
|
179
359
|
byte-identical receipts.
|
|
360
|
+
- **Not an interceptor of HTTP-transport MCP servers.** v0.1.0 wraps
|
|
361
|
+
the stdio transport only. Hosted HTTP MCP servers (the Claude
|
|
362
|
+
Desktop "Add Connector" URL pattern, e.g.
|
|
363
|
+
`https://mcp.notion.com/mcp`) are not in scope — the client talks
|
|
364
|
+
to them over HTTPS directly and no local process exists to wrap.
|
|
365
|
+
Use the upstream's local stdio server in the meantime; the
|
|
366
|
+
HTTP-transport story is Posture B in
|
|
367
|
+
[`docs/architecture/mcp-mode-3-enforcement-v1.md`](https://github.com/Strixgov/strix/tree/main/docs/architecture/mcp-mode-3-enforcement-v1.md).
|
|
180
368
|
- **Not Mode 3 today.** v0.1.0 ships Mode 1 + Mode 2. Mode 3
|
|
181
369
|
enforcement is the v0.2.0 upgrade path, designed for, but not
|
|
182
370
|
implemented in, this release.
|
|
371
|
+
- **Not a credential vault.** Upstream tokens (`NOTION_TOKEN`,
|
|
372
|
+
`GITHUB_TOKEN`, etc.) flow through `process.env` or
|
|
373
|
+
`upstream.env` and reach the upstream MCP server as-is. The
|
|
374
|
+
proxy does not store them, and they never appear in signed
|
|
375
|
+
receipts — receipts carry only the `invocationHash` (SHA-256 of
|
|
376
|
+
the canonical-JSON of the args), not the raw args themselves.
|
|
377
|
+
Token persistence is on the roadmap — a stopgap OS-keychain
|
|
378
|
+
integration and a full OAuth credential broker are both tracked.
|
|
379
|
+
|
|
380
|
+
## Known issues from real-world dogfood
|
|
381
|
+
|
|
382
|
+
A handful of failure modes that real first-run integrations have hit.
|
|
383
|
+
Each one has a one-line fix below.
|
|
384
|
+
|
|
385
|
+
| Symptom | Root cause | Fix |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| Proxy exits at startup with `[strix-proxy] startup failed: Cannot find package '@strixgov/capabilities-mcp-common'` | Companion pack not installed alongside globally-installed proxy. Fixed in `0.1.1+`. | Upgrade: `npm install -g @strixgov/mcp-proxy@latest`. On older versions, install the pack the same way: `npm install -g @strixgov/capabilities-mcp-common`. |
|
|
388
|
+
| Proxy starts, lists tools, but every Notion tool call gets `"decision":"DENY"` even for benign reads | Upstream is the modern `@notionhq/notion-mcp-server` (emits `API-*` tool names), but `@strixgov/capabilities-mcp-common` is at `0.1.0` (only knew the older `notion-*` names). Every call falls through to the heuristic MEDIUM classifier and gets gated. | Upgrade the pack: `npm install -g @strixgov/capabilities-mcp-common@latest` — `0.1.1+` covers both naming conventions (38 Notion tools). |
|
|
389
|
+
| Proxy starts cleanly, exposes the tools list, but tool invocations silently fail (no `tools/call` in Claude Desktop logs, no receipt written) | Observed on Windows + nvm: the upstream MCP server can stall when `@modelcontextprotocol/sdk` isn't resolvable from the proxy's working directory. Cause is environmental (npx + nvm + the upstream's peer-dep resolution), not a proxy bug, but the symptom is opaque enough to be worth documenting. | Belt-and-braces: `cd <storagePath dir> && npm install @modelcontextprotocol/sdk`, then full-quit + relaunch Claude Desktop. |
|
|
390
|
+
| `npx @strixgov/verifier` prints `KEY_NOT_FOUND` for a local-mode receipt | The proxy generates a local signing key (e.g. `signingKeyId: "strix-notion"`) that's not in the public JWKS at `well-known.strixgov.com`. Local mode is intentionally self-rooted. From v0.1.4+ the public JWK is persisted by default at `~/.strix-mcp-proxy/<serverId>/keys/public-jwk.json`. | Point the verifier at the persisted JWK: `npx @strixgov/verifier chain ~/.strix-mcp-proxy/<serverId>/receipts.jsonl --jwks ~/.strix-mcp-proxy/<serverId>/keys/public-jwk.json`. Receipts written under v0.1.0–v0.1.3 with `storagePath` unset (or aliased to `~/.strix-gateway`) are NOT recoverable — their keys were ephemeral. For JWKS-resolvable kids set `STRIX_API_KEY` + `STRIX_TENANT_ID` (connected mode). |
|
|
391
|
+
| `strix-gateway verify` prints `kid not in keyring: strix-<serverId>` for every proxy receipt | You're running `strix-gateway` (gateway CLI) against receipts written by `@strixgov/mcp-proxy`. They are separate stores with incompatible key layouts: the gateway uses a multi-kid keyring (`active` pointer + `<kid>/` subdirs); the proxy writes a flat single-key file. If you also pointed the proxy's `storagePath` at `~/.strix-gateway`, the proxy's key was never registered in the keyring. | Use the package-matched verifier: `npx @strixgov/verifier chain ~/.strix-mcp-proxy/<serverId>/receipts.jsonl --jwks ~/.strix-mcp-proxy/<serverId>/keys/public-jwk.json`. Going forward, leave `storagePath` at its v0.1.4 default (or set it to a path you control that is NOT `~/.strix-gateway`). |
|
|
392
|
+
| Proxy starts, exposes tools, allows reads, but every write or otherwise `APPROVAL_REQUIRED`-tier call returns `"decision":"DENY"` with `reason:"PROMPT_FAILED"` even though `approval.enabled` is `true` | The gateway's default approver is `terminalApprove`, which needs a TTY on stdin. Under Claude Desktop spawn the proxy's stdio is owned by the MCP protocol — there's no terminal, so the prompt fails closed. Fixed in `0.1.3+` via `approval.type: "file"` / `"auto"`. | Upgrade: `npm install -g @strixgov/mcp-proxy@latest` and set `"approval": { "enabled": true, "type": "file", "requestDir": "~/.strix-gateway/approvals" }` (real approval, headless) or `"approval": { "enabled": true, "type": "auto" }` (demo only — auto-approves everything). See "Approval modes" above. |
|
|
183
393
|
|
|
184
394
|
## Security posture
|
|
185
395
|
|
|
@@ -203,10 +413,7 @@ posture, including:
|
|
|
203
413
|
responsibility).
|
|
204
414
|
|
|
205
415
|
Report security issues via GitHub Security Advisory on
|
|
206
|
-
`github.com/Strixgov/strix`.
|
|
207
|
-
[`docs/runbooks/operate-mcp-proxy.md`](https://github.com/Tarshann/strix-platform/tree/main/docs/runbooks/operate-mcp-proxy.md)
|
|
208
|
-
covers daily operations, receipt-log rotation, and upgrade
|
|
209
|
-
discipline.
|
|
416
|
+
`github.com/Strixgov/strix`.
|
|
210
417
|
|
|
211
418
|
## License
|
|
212
419
|
|
package/SECURITY.md
CHANGED
|
@@ -6,9 +6,10 @@ It answers the questions a security review will ask in the first
|
|
|
6
6
|
which threats does it cover or not cover.
|
|
7
7
|
|
|
8
8
|
For the broader Strix threat model (the platform, the verifier,
|
|
9
|
-
the kernel-binding guarantees), see
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
the kernel-binding guarantees), see the threat-model documentation
|
|
10
|
+
distributed with the Strix platform. Authorized reviewers and
|
|
11
|
+
prospective customers can request the current version via
|
|
12
|
+
security@strixgov.com.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -197,7 +198,7 @@ The proxy is a single Node.js process. Crash semantics:
|
|
|
197
198
|
| Proxy crashes mid-tool-call | Upstream call may or may not have started (depends on timing). No receipt is written. The MCP client sees a transport error. | Restart proxy. The action did not execute in any auditable sense. Re-running the same agent input replays the call from scratch. |
|
|
198
199
|
| Upstream crashes during a call | Proxy receives the error from the upstream client. Receipt is written with `decision: "DENY"` (or `"ALLOW"` if policy already approved + upstream failed AFTER consume). Error propagated to client. | Standard MCP error handling on the client side. |
|
|
199
200
|
| Proxy crashes mid-approval | Pending approval is lost (in-memory queue). The held action does not execute. | Restart proxy. The action would need to be re-attempted; the previous approval is gone. |
|
|
200
|
-
| Disk fills up (with `storagePath` set) | New receipt writes fail. The proxy raises an error but does not fall back to silent ALLOW — fail-closed discipline. | Rotate / archive receipts.jsonl
|
|
201
|
+
| Disk fills up (with `storagePath` set) | New receipt writes fail. The proxy raises an error but does not fall back to silent ALLOW — fail-closed discipline. | Rotate / archive `receipts.jsonl` before restarting. |
|
|
201
202
|
| Signing key not loadable | Constructor throws at startup. **The proxy does not start.** | Fix the key configuration. There is no "governance degraded but tool still runs" state. |
|
|
202
203
|
|
|
203
204
|
### 4.1 Receipt-log rotation is operator-owned
|
|
@@ -214,8 +215,7 @@ operator-side:
|
|
|
214
215
|
`receipts.jsonl` then becomes a transient buffer that can be
|
|
215
216
|
rotated aggressively.
|
|
216
217
|
- Anyone running the proxy at non-trivial volume MUST decide on a
|
|
217
|
-
rotation policy before launching.
|
|
218
|
-
[`docs/runbooks/operate-mcp-proxy.md`](https://github.com/Tarshann/strix-platform/tree/main/docs/runbooks/operate-mcp-proxy.md) §3.
|
|
218
|
+
rotation policy before launching.
|
|
219
219
|
|
|
220
220
|
This is intentionally not in the proxy itself — log rotation
|
|
221
221
|
strategy is a deployment decision (Node-level `logrotate-stream`?
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"_comment_upstream_env": "Two patterns for the NOTION_TOKEN. Pattern A (recommended for Claude Desktop): omit upstream.env and set NOTION_TOKEN in Claude Desktop's env block — it propagates to the upstream via process.env. Pattern B (explicit): add upstream.env here, e.g. \"env\": { \"NOTION_TOKEN\": \"secret_...\" }. See README 'Where the upstream's secrets should live' for trade-offs.",
|
|
2
3
|
"serverId": "notion",
|
|
3
4
|
"upstream": {
|
|
4
5
|
"command": "npx",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strixgov/mcp-proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Strix MCP Proxy — the distribution wedge for governed agent tool execution. Point your MCP client at the proxy instead of the underlying server; every callTool is intercepted, evaluated against policy, and emits a signed Ed25519 receipt. Designed so Mode 3 (capability-enforced) is the v0.2.0 upgrade path, not a rewrite.",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -17,11 +17,9 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@strixgov/mcp-adapter": "^0.1.0",
|
|
19
19
|
"@strixgov/tool-gateway": "^0.4.1",
|
|
20
|
+
"@strixgov/capabilities-mcp-common": "^0.1.0",
|
|
20
21
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
21
22
|
},
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"@strixgov/capabilities-mcp-common": "0.1.0"
|
|
24
|
-
},
|
|
25
23
|
"keywords": [
|
|
26
24
|
"strix",
|
|
27
25
|
"mcp",
|
package/src/config.mjs
CHANGED
|
@@ -75,7 +75,32 @@ export async function resolveCapabilities(capabilities) {
|
|
|
75
75
|
"resolveCapabilities: 'capabilities' must be a string import path or an array",
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
let mod;
|
|
79
|
+
try {
|
|
80
|
+
mod = await import(capabilities);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// ERR_MODULE_NOT_FOUND is the common case when a globally-installed
|
|
83
|
+
// proxy can't resolve a companion pack that wasn't installed alongside
|
|
84
|
+
// it. Surface an actionable error rather than the bare Node loader
|
|
85
|
+
// message, since "Cannot find package '...'" gives the operator no
|
|
86
|
+
// hint that the fix is `npm install -g <the-package>`.
|
|
87
|
+
if (err && (err.code === "ERR_MODULE_NOT_FOUND" || /Cannot find package/i.test(err.message))) {
|
|
88
|
+
// Best-effort: peel off the subpath so the suggested install
|
|
89
|
+
// command targets the bare package name. "@strixgov/capabilities-mcp-common/notion"
|
|
90
|
+
// → "@strixgov/capabilities-mcp-common".
|
|
91
|
+
const bare = capabilities.startsWith("@")
|
|
92
|
+
? capabilities.split("/").slice(0, 2).join("/")
|
|
93
|
+
: capabilities.split("/")[0];
|
|
94
|
+
throw new Error(
|
|
95
|
+
`resolveCapabilities: cannot load capability pack '${capabilities}'. ` +
|
|
96
|
+
`If you installed @strixgov/mcp-proxy globally, install the pack the same way: ` +
|
|
97
|
+
`\`npm install -g ${bare}\`. ` +
|
|
98
|
+
`Otherwise add '${bare}' to your project's dependencies. ` +
|
|
99
|
+
`(underlying error: ${err.message})`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
79
104
|
// Look for the conventional name (foo → fooCapabilities, *Capabilities,
|
|
80
105
|
// default, or the first exported array). The companion packs use the
|
|
81
106
|
// `<server>Capabilities` convention, so check that first.
|
package/src/proxy.mjs
CHANGED
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
* maximum coverage of the actual MCP-call surface.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import os from "node:os";
|
|
32
34
|
import { governMCPServer } from "@strixgov/mcp-adapter";
|
|
35
|
+
import { loadOrCreateSigningKey, fileApprover } from "@strixgov/tool-gateway";
|
|
33
36
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
34
37
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
35
38
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -72,6 +75,34 @@ export async function startProxy(opts) {
|
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
// v0.1.4: default storagePath to ~/.strix-mcp-proxy/<serverId> when unset.
|
|
79
|
+
//
|
|
80
|
+
// Pre-0.1.4 left opts.storagePath undefined, which caused governMCPServer
|
|
81
|
+
// to default both halves of persistence wrong:
|
|
82
|
+
// - storage = MemoryStorage (receipts vanish with the process)
|
|
83
|
+
// - signingKey = ephemeral generateSigningKey() (kid is in every receipt
|
|
84
|
+
// on disk, but the actual key is gone the moment the proxy exits)
|
|
85
|
+
// The combination silently produced "persistent receipts signed by a key
|
|
86
|
+
// that no longer exists anywhere", which is the worst possible state —
|
|
87
|
+
// operators who pointed storagePath at ~/.strix-gateway were getting
|
|
88
|
+
// receipts they could list but never verify.
|
|
89
|
+
//
|
|
90
|
+
// Per-serverId default (not a single shared dir) so multiple wrapped
|
|
91
|
+
// upstreams cleanly partition their receipts + keys. Intentionally NOT
|
|
92
|
+
// ~/.strix-gateway: the gateway CLI uses a multi-kid keyring layout
|
|
93
|
+
// (active pointer + <kid>/ subdirs) and the proxy uses loadOrCreateSigningKey's
|
|
94
|
+
// flat single-key layout — sharing the same keys/ dir collides silently
|
|
95
|
+
// (proxy's flat files are invisible to the keyring loader, OR a stale
|
|
96
|
+
// migration clobbers the gateway's active pointer to the proxy's kid).
|
|
97
|
+
//
|
|
98
|
+
// Opt out by passing `storagePath: null` (treated as "I really mean
|
|
99
|
+
// ephemeral"). Caller-supplied opts.signingKey still wins regardless of
|
|
100
|
+
// storage choice.
|
|
101
|
+
const resolvedStoragePath =
|
|
102
|
+
opts.storagePath === null
|
|
103
|
+
? null
|
|
104
|
+
: opts.storagePath ?? path.join(os.homedir(), ".strix-mcp-proxy", serverId);
|
|
105
|
+
|
|
75
106
|
// ─── 1. Connect to the upstream MCP server ─────────────────────────────────
|
|
76
107
|
// Test mode lets callers pre-build the upstream client (e.g. with an
|
|
77
108
|
// InMemoryTransport pair) so the proxy can be exercised end-to-end
|
|
@@ -97,14 +128,72 @@ export async function startProxy(opts) {
|
|
|
97
128
|
// ─── 2. Build the iface mcp-adapter expects ───────────────────────────────
|
|
98
129
|
const upstreamIface = buildUpstreamIface(upstreamClient, serverId);
|
|
99
130
|
|
|
100
|
-
// ───
|
|
131
|
+
// ─── 3a. Resolve a persistent signing key when storagePath is set ──────────
|
|
132
|
+
// Without this, governMCPServer falls back to an in-memory ephemeral key
|
|
133
|
+
// (`generateSigningKey(\`strix-${serverId}\`)`) — meaning every restart
|
|
134
|
+
// produces a new key with the same kid and the previous key (the one that
|
|
135
|
+
// signed receipts already on disk) is gone forever. JSONL receipts on disk
|
|
136
|
+
// would then be unverifiable: the kid points at a key that doesn't exist
|
|
137
|
+
// anywhere, including the proxy that wrote them.
|
|
138
|
+
//
|
|
139
|
+
// When storagePath is set the operator clearly wants persistent receipts;
|
|
140
|
+
// it's the natural moment to also persist the signing key alongside them.
|
|
141
|
+
// The keypair is written to `<storagePath>/keys/{signing-key.pem, public-jwk.json}`
|
|
142
|
+
// with PKCS8 PEM at 0600 / public JWK at 0644 (mirrors loadOrCreateSigningKey's
|
|
143
|
+
// own discipline). Caller-supplied `opts.signingKey` still wins.
|
|
144
|
+
let resolvedSigningKey = opts.signingKey;
|
|
145
|
+
if (!resolvedSigningKey && resolvedStoragePath) {
|
|
146
|
+
resolvedSigningKey = await loadOrCreateSigningKey({
|
|
147
|
+
keyPath: path.join(resolvedStoragePath, "keys"),
|
|
148
|
+
kid: `strix-${serverId}`,
|
|
149
|
+
});
|
|
150
|
+
audit(opts, {
|
|
151
|
+
kind: "signing.key.resolved",
|
|
152
|
+
detail: {
|
|
153
|
+
source: opts.storagePath ? "persistent" : "persistent-default",
|
|
154
|
+
kid: resolvedSigningKey.kid,
|
|
155
|
+
keyPath: path.join(resolvedStoragePath, "keys"),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── 3b. Resolve approval config to an approver function ───────────────────
|
|
161
|
+
// The gateway's approval expects `prompt: (cap, inv, callOpts) => Promise<...>`
|
|
162
|
+
// — a function. JSON config can't carry a function, so the proxy translates
|
|
163
|
+
// a string `approval.type` to the right approver from @strixgov/tool-gateway.
|
|
164
|
+
//
|
|
165
|
+
// Why this matters: when the proxy is spawned by Claude Desktop (or any
|
|
166
|
+
// other MCP client that owns the stdio channel), the default `terminalApprove`
|
|
167
|
+
// fails closed because stdin/stdout aren't a TTY — every APPROVAL_REQUIRED
|
|
168
|
+
// call gets denied with `PROMPT_FAILED`. Operators have to pick a headless
|
|
169
|
+
// approver explicitly. `type: "file"` is the headless default; `type: "auto"`
|
|
170
|
+
// is the demo shortcut; `type: "terminal"` is the legacy default (works only
|
|
171
|
+
// when the proxy is run from a real terminal).
|
|
172
|
+
//
|
|
173
|
+
// Supported config shapes (all under `approval`):
|
|
174
|
+
//
|
|
175
|
+
// {enabled: false} ← gate disabled, every APPROVAL_REQUIRED → DENY
|
|
176
|
+
// {enabled: true} ← terminal (legacy default; fails closed in Claude Desktop)
|
|
177
|
+
// {enabled: true, type: "terminal"} ← terminal (explicit)
|
|
178
|
+
// {enabled: true, type: "auto"} ← shortcut for autoApprove: true (demos)
|
|
179
|
+
// {enabled: true, type: "file",
|
|
180
|
+
// requestDir: "~/.strix-gateway/approvals", ← request/response file polling (headless)
|
|
181
|
+
// timeoutMs: 300000}
|
|
182
|
+
// {enabled: true, autoApprove: true} ← long form of {type: "auto"}
|
|
183
|
+
// {enabled: true, prompt: <fn>} ← programmatic caller wins (always)
|
|
184
|
+
const resolvedApproval = resolveApproval(opts.approval, {
|
|
185
|
+
...opts,
|
|
186
|
+
storagePath: resolvedStoragePath,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ─── 3c. Wrap with governMCPServer (Mode 1/2) ──────────────────────────────
|
|
101
190
|
const governed = governMCPServer(upstreamIface, {
|
|
102
191
|
serverId,
|
|
103
192
|
capabilities: opts.capabilities,
|
|
104
193
|
policy: opts.policy ?? { default: "DENY" },
|
|
105
|
-
approval:
|
|
106
|
-
storagePath:
|
|
107
|
-
signingKey:
|
|
194
|
+
approval: resolvedApproval,
|
|
195
|
+
storagePath: resolvedStoragePath,
|
|
196
|
+
signingKey: resolvedSigningKey,
|
|
108
197
|
connectedMode: opts.connectedMode,
|
|
109
198
|
});
|
|
110
199
|
|
|
@@ -210,3 +299,85 @@ function audit(opts, event) {
|
|
|
210
299
|
try { opts.onAudit(event); } catch { /* swallow audit-handler errors */ }
|
|
211
300
|
}
|
|
212
301
|
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Translate the JSON-friendly `approval` config to the function-shaped
|
|
305
|
+
* approval object the gateway expects.
|
|
306
|
+
*
|
|
307
|
+
* Resolution rules (first match wins):
|
|
308
|
+
*
|
|
309
|
+
* 1. `approval.prompt` is a function → pass through unchanged
|
|
310
|
+
* (programmatic callers can always inject their own approver).
|
|
311
|
+
* 2. `approval.enabled === false` → pass through unchanged
|
|
312
|
+
* (no prompt needed; gateway denies every APPROVAL_REQUIRED).
|
|
313
|
+
* 3. `approval.autoApprove === true` → pass through unchanged
|
|
314
|
+
* (long-form alias for type: "auto").
|
|
315
|
+
* 4. `approval.type === "auto"` → set autoApprove: true.
|
|
316
|
+
* 5. `approval.type === "file"` → construct fileApprover.
|
|
317
|
+
* `requestDir` defaults to
|
|
318
|
+
* `<storagePath>/approvals` if
|
|
319
|
+
* storagePath is set, otherwise
|
|
320
|
+
* `./.strix-approvals`.
|
|
321
|
+
* `~` and `$HOME` are expanded.
|
|
322
|
+
* 6. `approval.type === "terminal"` → pass through unchanged
|
|
323
|
+
* (gateway defaults to terminalApprove). Fails closed in non-TTY
|
|
324
|
+
* environments — fine for direct-CLI testing, broken under
|
|
325
|
+
* Claude Desktop spawn.
|
|
326
|
+
* 7. Unknown `approval.type` → throw at startup so the
|
|
327
|
+
* operator can't misconfigure silently into a fail-closed loop.
|
|
328
|
+
*/
|
|
329
|
+
function resolveApproval(approval, opts) {
|
|
330
|
+
if (!approval) return { enabled: false };
|
|
331
|
+
|
|
332
|
+
// Programmatic callers always win — JSON-config strings are a
|
|
333
|
+
// convenience for operators, not the source of truth.
|
|
334
|
+
if (typeof approval.prompt === "function") return approval;
|
|
335
|
+
if (approval.enabled === false) return approval;
|
|
336
|
+
if (approval.autoApprove === true) return approval;
|
|
337
|
+
|
|
338
|
+
const type = approval.type;
|
|
339
|
+
if (type === undefined || type === null) return approval;
|
|
340
|
+
|
|
341
|
+
if (type === "auto") {
|
|
342
|
+
return { ...approval, autoApprove: true };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (type === "file") {
|
|
346
|
+
const requestDirRaw =
|
|
347
|
+
approval.requestDir ??
|
|
348
|
+
(opts.storagePath ? path.join(opts.storagePath, "approvals") : "./.strix-approvals");
|
|
349
|
+
const requestDir = expandHomeTilde(requestDirRaw);
|
|
350
|
+
return {
|
|
351
|
+
...approval,
|
|
352
|
+
prompt: fileApprover({
|
|
353
|
+
requestDir,
|
|
354
|
+
timeoutMs: approval.timeoutMs,
|
|
355
|
+
pollIntervalMs: approval.pollIntervalMs,
|
|
356
|
+
}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (type === "terminal") {
|
|
361
|
+
// Pass through; gateway's default is terminalApprove. Operators who
|
|
362
|
+
// type this explicitly are accepting the non-TTY fail-closed behavior.
|
|
363
|
+
return approval;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
throw new Error(
|
|
367
|
+
`startProxy: unknown approval.type '${type}' — expected "terminal", "file", or "auto"`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Expand a leading `~` or `~/` to the OS user-home directory. Mirrors the
|
|
373
|
+
* tilde-expansion behavior in loadConfig so `~/.strix-gateway/approvals`
|
|
374
|
+
* resolves consistently across config locations.
|
|
375
|
+
*/
|
|
376
|
+
function expandHomeTilde(p) {
|
|
377
|
+
if (typeof p !== "string") return p;
|
|
378
|
+
if (p === "~") return os.homedir();
|
|
379
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) {
|
|
380
|
+
return path.join(os.homedir(), p.slice(2));
|
|
381
|
+
}
|
|
382
|
+
return p;
|
|
383
|
+
}
|