@strixgov/mcp-proxy 0.1.0

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/SECURITY.md ADDED
@@ -0,0 +1,293 @@
1
+ # Security posture — `@strixgov/mcp-proxy`
2
+
3
+ This document is the security-team-facing surface for the package.
4
+ It answers the questions a security review will ask in the first
5
+ 30 minutes — what does the package hold, where, for how long, and
6
+ which threats does it cover or not cover.
7
+
8
+ For the broader Strix threat model (the platform, the verifier,
9
+ the kernel-binding guarantees), see
10
+ [`docs/security/THREAT-MODEL.md`](https://github.com/Tarshann/strix-platform/tree/main/docs/security/THREAT-MODEL.md)
11
+ in the source repository.
12
+
13
+ ---
14
+
15
+ ## 1. Scope
16
+
17
+ This document covers `@strixgov/mcp-proxy` — the standalone process
18
+ that wraps an upstream MCP server. It applies to v0.1.0 and any
19
+ patch release of that line.
20
+
21
+ Out of scope:
22
+
23
+ - The trust path of receipt verification — handled by
24
+ [`@strixgov/verifier`](https://www.npmjs.com/package/@strixgov/verifier)
25
+ (MIT, zero-dependency, separate trust root).
26
+ - The execution-authorization token format — handled by
27
+ [`@strixgov/mcp-token-validator`](https://www.npmjs.com/package/@strixgov/mcp-token-validator),
28
+ which lands as the Mode 3 enforcement primitive when wiring ships
29
+ in v0.2.0.
30
+ - The Strix Console + the hosted Connected Mode platform — separate
31
+ audit posture.
32
+
33
+ ---
34
+
35
+ ## 2. Credential boundary — where do upstream credentials live?
36
+
37
+ **On disk: never, by design.** The proxy writes signed receipts
38
+ (`receipts.jsonl`) and nothing else. No `.env` file, no credential
39
+ cache, no token store. If you see secrets on disk, something in
40
+ your wrapper script or logging is putting them there — not the
41
+ proxy.
42
+
43
+ **In memory: depends on which architectural position you deploy.**
44
+ The proxy itself doesn't pin you to one of the three positions
45
+ below; the choice is an integration decision your operator makes.
46
+
47
+ ### Position A — proxy holds the credential
48
+
49
+ The proxy fetches an upstream credential from a secrets manager
50
+ (or reads it from its own environment) and passes it downstream:
51
+ as a child-process env var (stdio MCP), as an `Authorization`
52
+ header on outbound HTTP, or as an SDK initialization parameter.
53
+
54
+ | | |
55
+ |---|---|
56
+ | **Credential lives in** | Proxy heap + downstream sink (child env, fetch headers, SDK client object) |
57
+ | **Lifetime** | Proxy process lifetime |
58
+ | **Who can read it** | Anyone with `ptrace` / `/proc/<pid>/mem` access to the proxy UID |
59
+ | **Used when** | Upstream is a binary you spawn (stdio MCP servers), OR an HTTP endpoint where the proxy is the OAuth client |
60
+ | **Audit posture** | Proxy is in scope for credential-handling audit. Treat it as a credential-bearing system. |
61
+
62
+ ### Position B — proxy passes the credential through
63
+
64
+ Client (e.g. Claude Code / Claude Desktop) supplies the credential
65
+ per-request (header, bearer token). Proxy forwards it to upstream
66
+ unchanged. Proxy sees the bytes in flight but does not retain them
67
+ past the request.
68
+
69
+ | | |
70
+ |---|---|
71
+ | **Credential lives in** | Per-request heap, GC'd after the response |
72
+ | **Lifetime** | Single request |
73
+ | **Who can read it** | Same as Position A, but only during the request window |
74
+ | **Used when** | Upstream is hosted HTTP and supports OAuth; client is the OAuth client; transparent passthrough |
75
+ | **Audit posture** | Proxy is not a credential-bearing system in steady state. Receipts cannot cryptographically attest actor identity beyond what the client supplies. |
76
+
77
+ ### Position C — proxy never sees the credential
78
+
79
+ Upstream identity is resolved out-of-band: Workload Identity on
80
+ cloud platforms, IAM role on the instance, OS keychain accessed
81
+ directly by the upstream process, or the upstream binary resolves
82
+ its own secrets from a secrets manager.
83
+
84
+ | | |
85
+ |---|---|
86
+ | **Credential lives in** | Only the upstream's process / cloud metadata service |
87
+ | **Lifetime** | Upstream lifetime |
88
+ | **Who can read it** | Same UID as the upstream process |
89
+ | **Used when** | Cloud-native deployments; any upstream that supports native secret resolution |
90
+ | **Audit posture** | Cleanest. Proxy is not credential-bearing at all. The credential never crosses the proxy boundary. |
91
+
92
+ ### The decision rule
93
+
94
+ If your auditor needs to confirm the proxy is **not** a
95
+ credential-bearing system, the answer depends entirely on whether
96
+ you deployed it as Position A, B, or C. The proxy itself does not
97
+ choose for you.
98
+
99
+ | If your deployment supports it | Use |
100
+ |---|---|
101
+ | Any major cloud / on-prem with Vault Agent or equivalent | **Position C** — cleanest audit story |
102
+ | Upstream is hosted HTTP + supports OAuth + client can be the OAuth client + you don't need cryptographically-verified actor identity in receipts | **Position B** — proxy stays credential-free |
103
+ | Upstream is a binary you spawn (stdio MCP) OR you need the proxy to be the identity broker | **Position A** — proxy is in scope for credential-handling audit, but the receipt's claimed actor identity can be cryptographically attested once AA-1 wiring ships |
104
+
105
+ Same primitive, three deployment shapes, materially different audit
106
+ and threat-model profiles.
107
+
108
+ ---
109
+
110
+ ## 3. What the proxy stores regardless of position
111
+
112
+ The four artifacts the proxy holds in any deployment shape:
113
+
114
+ | Artifact | Contents | Sensitivity | Lifetime / Location |
115
+ |---|---|---|---|
116
+ | `receipts.jsonl` (or in-memory) | Signed canonical payloads: `capabilityId`, `action`, `decision`, `risk`, `mode`, `policyVersion`, `tenantId`, `environment`, **`invocationHash` (SHA-256 of args+actor)**, `evidenceHash`, `proofChainHash`, `timestamp`, `signature`, `signingKeyId`. Receipt also carries `actorId`, `actorRole`, and (if approved) `approvedBy` as un-signed display fields. | **Does NOT contain raw `args`** — only a hash. See §3.1 for what this means in practice. Never contains auth credentials. | On disk at `opts.storagePath` if set; in-memory otherwise (lost on process exit) |
117
+ | Gateway signing key | Ed25519 private key used to sign receipts | Generated per-install if not supplied; **distinct from upstream credentials** | Memory by default. Override via `opts.signingKey` to load from your secrets manager. Persistent storage is operator-managed. |
118
+ | Policy config | Capability rules, risk overrides, fail-closed defaults | **Not secret.** Check it into source control. | Read at startup from the config file or CLI flags |
119
+ | Approval gate state | Pending-approval queue (only if approvals enabled) | Holds pending-action metadata until decided | In-memory by default; lost on process exit unless you wire persistence |
120
+
121
+ ### 3.1 What the receipt does and does not contain
122
+
123
+ This is the most common question. The honest answer:
124
+
125
+ | The receipt stores | The receipt does NOT store |
126
+ |---|---|
127
+ | The **capability id** invoked (e.g. `mcp.notion.create_comment`) | The raw tool arguments — only their SHA-256 hash (`invocationHash`) |
128
+ | The **decision** (`ALLOW` / `DENY`) | The agent's prompt or reasoning |
129
+ | The **actor id + role** (e.g. `agent-claude-1` / `agent`) | The tool's response content |
130
+ | The **approver identity** if approval gate fired | The upstream API response body |
131
+ | Timestamps + signing-key id + proof-chain linkage | Auth credentials of any kind |
132
+
133
+ **Practical implication for receipt sharing:** because raw args
134
+ are NOT in the signed bytes or the on-disk record, you can share
135
+ the receipt JSONL externally **without args-redaction concerns**.
136
+ The signature verifies against `invocationHash`, which is derived
137
+ deterministically from the args + actor fields the agent supplied.
138
+ An auditor can confirm the signed receipt corresponds to a given
139
+ args bundle by hashing their copy of the args and comparing to
140
+ `invocationHash`. If they don't have the args, they can still
141
+ confirm the receipt is authentic, just not what it was authorizing.
142
+
143
+ This is by design — the receipt is the audit artifact; the args
144
+ themselves are operationally-sensitive call data that the proxy
145
+ **never persists**.
146
+
147
+ ### 3.2 In-memory args sensitivity
148
+
149
+ While a `callTool` is in flight, the proxy DOES see the raw args
150
+ in memory:
151
+
152
+ - Args come in from the MCP client via stdio
153
+ - Args flow through `governMCPServer` for policy evaluation +
154
+ hashing into `invocationHash`
155
+ - Args are forwarded to the upstream MCP server via the proxy's
156
+ internal client
157
+
158
+ During this window, an attacker with `ptrace` access to the proxy
159
+ UID can read the args. After the call returns, the args are eligible
160
+ for GC and the only persistent trace is `invocationHash`.
161
+
162
+ For deployments that need additional in-memory protection (e.g.
163
+ agent args contain regulated data even during a single call),
164
+ combine with a hardened deployment environment: dedicated UID,
165
+ seccomp/AppArmor profile that blocks `ptrace`, no co-tenant
166
+ processes under the same UID. Strix-side mitigations are not the
167
+ right layer.
168
+
169
+ ### 3.3 Signing key lifecycle
170
+
171
+ The Ed25519 signing key the gateway uses to sign receipts is:
172
+
173
+ - **Auto-generated** if not supplied (per-process; lost on restart
174
+ unless `opts.storagePath` is set and the key is persisted by
175
+ the underlying `@strixgov/tool-gateway` storage). This is fine
176
+ for development and Mode 1 demonstrations.
177
+ - **Caller-supplied** when `opts.signingKey` is set, OR when an
178
+ operator runs the tool-gateway's key-management commands to
179
+ persist a key under `~/.strix/keys/`. Use this in production.
180
+ - **Rotated** by replacing the key + publishing the old public key
181
+ to the JWKS for the retention window (default 2 years for EU AI
182
+ Act Article 12 compliance). Historical receipts remain
183
+ verifiable.
184
+
185
+ The signing key is **not the same as the upstream credential**.
186
+ Rotating one does not rotate the other; they have different
187
+ lifecycles and different blast radii.
188
+
189
+ ---
190
+
191
+ ## 4. Process model — what happens on failure
192
+
193
+ The proxy is a single Node.js process. Crash semantics:
194
+
195
+ | Failure mode | What happens | Recovery |
196
+ |---|---|---|
197
+ | 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
+ | 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
+ | 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 (see [`docs/runbooks/operate-mcp-proxy.md`](https://github.com/Tarshann/strix-platform/tree/main/docs/runbooks/operate-mcp-proxy.md) §3) before restarting. |
201
+ | 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
+ ### 4.1 Receipt-log rotation is operator-owned
204
+
205
+ `receipts.jsonl` has **no built-in rotation** in v0.1.0. A
206
+ high-volume deployment will fill disk. Mitigation is
207
+ operator-side:
208
+
209
+ - Standard `logrotate(8)` with copytruncate is the simplest path.
210
+ Configure it to rotate when the file exceeds your retention
211
+ comfort threshold.
212
+ - Connected Mode customers can configure the strixgov.com
213
+ platform to be the durable retention store; the local
214
+ `receipts.jsonl` then becomes a transient buffer that can be
215
+ rotated aggressively.
216
+ - Anyone running the proxy at non-trivial volume MUST decide on a
217
+ rotation policy before launching. See
218
+ [`docs/runbooks/operate-mcp-proxy.md`](https://github.com/Tarshann/strix-platform/tree/main/docs/runbooks/operate-mcp-proxy.md) §3.
219
+
220
+ This is intentionally not in the proxy itself — log rotation
221
+ strategy is a deployment decision (Node-level `logrotate-stream`?
222
+ journald? Connected Mode forwarding?) and baking one in would lock
223
+ operators out of their preferred tooling.
224
+
225
+ ---
226
+
227
+ ## 5. Multi-tenant boundary
228
+
229
+ The proxy is **single-tenant by design** in v0.1.0.
230
+
231
+ - One proxy process serves one set of upstream credentials, one
232
+ signing key, one receipt log.
233
+ - If you run multiple agents through one proxy, they share:
234
+ - The same upstream credentials (Position A)
235
+ - The same signing key
236
+ - The same `receipts.jsonl`
237
+ - The same approval queue
238
+
239
+ This is not a bug — it matches how MCP clients (Claude Desktop,
240
+ mcp-cli, IDE integrations) configure MCP servers: one upstream
241
+ per client config entry, with the proxy spawning one process per
242
+ entry.
243
+
244
+ **If you need per-tenant isolation:** run one proxy process per
245
+ tenant. Each gets its own credentials, signing key, receipt log,
246
+ and approval queue. Resource cost: small (Node process + ~30MB
247
+ RSS). This is the recommended pattern.
248
+
249
+ Future v0.2.0+ may introduce a multi-tenant variant. Until then,
250
+ **do not share a single proxy process across tenants in
251
+ production** without an explicit threat model that accepts the
252
+ shared-credential + shared-signing-key implications.
253
+
254
+ ---
255
+
256
+ ## 6. What the proxy does NOT govern
257
+
258
+ For audit clarity — the proxy's surface is bounded:
259
+
260
+ | Surface | Governed? | Why not |
261
+ |---|---|---|
262
+ | MCP `callTool` requests routed through the proxy | **YES** | The whole point. |
263
+ | MCP `listTools` / `initialize` / other protocol messages | **NO** | These are protocol overhead, not state-changing. No receipts emitted. |
264
+ | Direct API calls the agent makes outside the proxy | **NO** | Out-of-wrapper traffic is invisible to the proxy. Mode 3 (capability enforcement) is the answer here; not shipped in v0.1.0. |
265
+ | The agent's own internal state, prompts, or reasoning | **NO** | The proxy is at the action boundary, not upstream of the model. |
266
+ | The upstream MCP server's internal behavior | **NO** | The proxy trusts the upstream to honor the `callTool` request it forwards. SQL injection, command injection inside the upstream's argument handling — out of scope for the proxy. Mitigation is the upstream's own argument validation. |
267
+ | Network egress from the agent's runtime | **NO** | Network policy is the deployment's responsibility. Mode 3 Posture B (HTTP egress gating) is the architectural answer; Strix Proxy v0.1.0 is the stdio-only variant. |
268
+ | The proxy's own logging / observability backend | **NO** | If you forward proxy logs to a SIEM, you're responsible for what ends up there. The proxy writes audit-relevant content to stderr (lifecycle events) and stdout-MCP-protocol (the protocol channel); receipts go to `receipts.jsonl` or in-memory. |
269
+
270
+ ---
271
+
272
+ ## 7. Reporting security issues
273
+
274
+ Open a **GitHub Security Advisory** on the public mirror at
275
+ `github.com/Strixgov/strix` for any of:
276
+
277
+ - Cryptographic findings (signature verification bypass, key
278
+ recovery, canonicalization drift)
279
+ - Credential leakage paths in the proxy code
280
+ - Authentication / authorization gaps
281
+ - Receipt forgery vectors
282
+
283
+ For non-security operational issues, use the normal GitHub Issues
284
+ surface or the support channels documented in
285
+ [`COMMERCIAL.md`](./COMMERCIAL.md).
286
+
287
+ ---
288
+
289
+ ## 8. Version history of this document
290
+
291
+ | Version | Date | Change |
292
+ |---|---|---|
293
+ | 1.0 | 2026-05-23 | Initial publication alongside `@strixgov/mcp-proxy` v0.1.0 |
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strix-mcp-proxy — CLI entry.
4
+ *
5
+ * Two invocation modes:
6
+ *
7
+ * strix-mcp-proxy --config ./proxy-config.json
8
+ *
9
+ * strix-mcp-proxy \
10
+ * --upstream-command npx \
11
+ * --upstream-arg "-y" --upstream-arg "@notionhq/notion-mcp-server" \
12
+ * --server-id notion \
13
+ * --capabilities @strixgov/capabilities-mcp-common/notion \
14
+ * --policy-default DENY \
15
+ * --risk-low ALLOW --risk-medium APPROVAL_REQUIRED
16
+ *
17
+ * The CLI is intentionally minimal — anything elaborate goes in the
18
+ * config file. Operators wiring this into Claude Desktop, mcp-cli, or
19
+ * an IDE integration just change their existing MCP command from
20
+ * `<upstream>` to `strix-mcp-proxy --config <path>`.
21
+ *
22
+ * Audit events (proxy lifecycle + denied calls) print to stderr so the
23
+ * MCP stdio channel on stdout stays clean for the protocol.
24
+ */
25
+
26
+ import { startProxy, loadConfig, resolveCapabilities } from "../src/index.mjs";
27
+
28
+ const argv = process.argv.slice(2);
29
+ if (argv.includes("--help") || argv.includes("-h")) {
30
+ printHelp();
31
+ process.exit(0);
32
+ }
33
+ if (argv.includes("--version") || argv.includes("-v")) {
34
+ const { default: pkg } = await import("../package.json", { with: { type: "json" } });
35
+ console.log(`strix-mcp-proxy ${pkg.version}`);
36
+ process.exit(0);
37
+ }
38
+
39
+ try {
40
+ const opts = await parseArgs(argv);
41
+ const handle = await startProxy({
42
+ ...opts,
43
+ onAudit: (event) => {
44
+ // stderr: doesn't collide with the MCP stdio protocol on stdout.
45
+ console.error(`[strix-proxy] ${event.kind} ${JSON.stringify(event.detail)}`);
46
+ },
47
+ onReceipt: (r) => {
48
+ // One-line summary per receipt; full receipts persist via storagePath when set.
49
+ console.error(`[strix-proxy] receipt ${r.decision} ${r.capabilityId}`);
50
+ },
51
+ });
52
+
53
+ const shutdown = async (signal) => {
54
+ console.error(`[strix-proxy] shutting down (${signal})`);
55
+ try { await handle.stop(); } catch { /* best-effort */ }
56
+ process.exit(0);
57
+ };
58
+ process.on("SIGINT", () => shutdown("SIGINT"));
59
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
60
+ } catch (err) {
61
+ console.error(`[strix-proxy] startup failed: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ // ─── arg parsing ────────────────────────────────────────────────────────────
66
+
67
+ async function parseArgs(argv) {
68
+ const flag = (name) => {
69
+ const i = argv.indexOf(name);
70
+ return i >= 0 ? argv[i + 1] : undefined;
71
+ };
72
+ const flagAll = (name) => {
73
+ const out = [];
74
+ for (let i = 0; i < argv.length; i++) {
75
+ if (argv[i] === name) out.push(argv[i + 1]);
76
+ }
77
+ return out;
78
+ };
79
+
80
+ const configPath = flag("--config");
81
+ if (configPath) {
82
+ const cfg = await loadConfig(configPath);
83
+ return {
84
+ serverId: cfg.serverId,
85
+ upstream: cfg.upstream,
86
+ capabilities: await resolveCapabilities(cfg.capabilities),
87
+ policy: cfg.policy,
88
+ approval: cfg.approval,
89
+ storagePath: cfg.storagePath,
90
+ };
91
+ }
92
+
93
+ const upstreamCommand = flag("--upstream-command");
94
+ if (!upstreamCommand) {
95
+ throw new Error("missing --config or --upstream-command (run with --help)");
96
+ }
97
+ const upstreamArgs = flagAll("--upstream-arg");
98
+ const serverId = flag("--server-id") ?? "proxy";
99
+ const capabilitiesPath = flag("--capabilities");
100
+ const policyDefault = flag("--policy-default") ?? "DENY";
101
+ const riskOverrides = {};
102
+ for (const level of ["LOW", "MEDIUM", "HIGH", "CRITICAL"]) {
103
+ const v = flag(`--risk-${level.toLowerCase()}`);
104
+ if (v) riskOverrides[level] = v;
105
+ }
106
+ const approvalEnabled = argv.includes("--approval-enabled");
107
+ const storagePath = flag("--storage-path");
108
+
109
+ return {
110
+ serverId,
111
+ upstream: { command: upstreamCommand, args: upstreamArgs },
112
+ capabilities: capabilitiesPath ? await resolveCapabilities(capabilitiesPath) : undefined,
113
+ policy: {
114
+ riskOverrides: Object.keys(riskOverrides).length > 0 ? riskOverrides : undefined,
115
+ default: policyDefault,
116
+ },
117
+ approval: { enabled: approvalEnabled },
118
+ storagePath,
119
+ };
120
+ }
121
+
122
+ function printHelp() {
123
+ console.log(`strix-mcp-proxy — governed MCP proxy
124
+
125
+ USAGE
126
+ strix-mcp-proxy --config <path>
127
+ strix-mcp-proxy --upstream-command <cmd> [--upstream-arg <a> ...] [other flags]
128
+
129
+ CONFIG-FILE MODE
130
+ --config <path> Path to a JSON config file (see README for shape)
131
+
132
+ FLAG MODE
133
+ --upstream-command <cmd> Command to spawn the upstream MCP server (e.g. "npx")
134
+ --upstream-arg <arg> One arg for the upstream command (repeatable)
135
+ --server-id <id> Namespace prefix for capability ids (default: "proxy")
136
+ --capabilities <pkg> Companion-pack import path
137
+ (e.g. @strixgov/capabilities-mcp-common/notion)
138
+ --policy-default <D> Default decision for unmatched calls: ALLOW or DENY
139
+ (default: DENY)
140
+ --risk-low <D> Override decision for LOW-risk calls
141
+ --risk-medium <D> Override decision for MEDIUM-risk calls
142
+ --risk-high <D> Override decision for HIGH-risk calls
143
+ --risk-critical <D> Override decision for CRITICAL-risk calls
144
+ --approval-enabled Enable the approval gate (requires custom prompt)
145
+ --storage-path <dir> Persist receipts to JSONL at this directory
146
+ (default: in-memory)
147
+
148
+ GENERAL
149
+ --help, -h Show this help
150
+ --version, -v Show proxy version
151
+
152
+ The proxy speaks MCP over stdio toward its consumer. Configure your
153
+ MCP-aware client (Claude Desktop, mcp-cli, etc.) to spawn this binary
154
+ in place of the upstream MCP server you previously used directly.
155
+ `);
156
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "serverId": "notion",
3
+ "upstream": {
4
+ "command": "npx",
5
+ "args": ["-y", "@notionhq/notion-mcp-server"]
6
+ },
7
+ "capabilities": "@strixgov/capabilities-mcp-common/notion",
8
+ "policy": {
9
+ "riskOverrides": {
10
+ "LOW": "ALLOW",
11
+ "MEDIUM": "APPROVAL_REQUIRED",
12
+ "HIGH": "APPROVAL_REQUIRED",
13
+ "CRITICAL": "DENY"
14
+ },
15
+ "default": "DENY"
16
+ },
17
+ "approval": { "enabled": false },
18
+ "storagePath": "~/.strix-mcp-proxy/notion"
19
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@strixgov/mcp-proxy",
3
+ "version": "0.1.0",
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
+ "license": "Elastic-2.0",
6
+ "type": "module",
7
+ "main": "./src/index.mjs",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/types.d.ts",
11
+ "import": "./src/index.mjs"
12
+ }
13
+ },
14
+ "bin": {
15
+ "strix-mcp-proxy": "bin/strix-mcp-proxy.mjs"
16
+ },
17
+ "dependencies": {
18
+ "@strixgov/mcp-adapter": "^0.1.0",
19
+ "@strixgov/tool-gateway": "^0.4.1",
20
+ "@modelcontextprotocol/sdk": "^1.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@strixgov/capabilities-mcp-common": "0.1.0"
24
+ },
25
+ "keywords": [
26
+ "strix",
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "proxy",
30
+ "gateway",
31
+ "governance",
32
+ "ai-agents",
33
+ "execution-control",
34
+ "signed-receipts",
35
+ "capability-enforcement"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Strixgov/strix",
40
+ "directory": "packages/strixgov-mcp-proxy"
41
+ },
42
+ "files": [
43
+ "src/",
44
+ "bin/",
45
+ "examples/",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "COMMERCIAL.md",
49
+ "SECURITY.md",
50
+ "LICENSE"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "engines": {
56
+ "node": ">=18.0.0"
57
+ },
58
+ "scripts": {
59
+ "test": "node --test test/*.test.mjs"
60
+ }
61
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Proxy config loader.
3
+ *
4
+ * Config file shape (JSON, no inline comments):
5
+ *
6
+ * {
7
+ * "serverId": "notion",
8
+ * "upstream": {
9
+ * "command": "npx",
10
+ * "args": ["-y", "@notionhq/notion-mcp-server"]
11
+ * },
12
+ * "capabilities": "@strixgov/capabilities-mcp-common/notion", // dynamic import path
13
+ * "policy": {
14
+ * "riskOverrides": { "LOW": "ALLOW", "MEDIUM": "APPROVAL_REQUIRED", "CRITICAL": "DENY" },
15
+ * "default": "DENY"
16
+ * },
17
+ * "approval": { "enabled": false },
18
+ * "storagePath": "~/.strix-mcp-proxy/receipts"
19
+ * }
20
+ *
21
+ * The `capabilities` field is either:
22
+ * - a bare import path (resolved via dynamic import; must export
23
+ * a named or default ReadonlyArray<McpCapability>), or
24
+ * - an inline array (already-classified caller-managed list)
25
+ *
26
+ * No env-var substitution in v0.1.0 — operators that need secrets
27
+ * inject them via `upstream.env` from the surrounding shell rather
28
+ * than embedding them in the config file.
29
+ */
30
+
31
+ import fs from "node:fs/promises";
32
+ import path from "node:path";
33
+ import os from "node:os";
34
+
35
+ /**
36
+ * @param {string} filePath
37
+ * @returns {Promise<object>}
38
+ */
39
+ export async function loadConfig(filePath) {
40
+ const abs = path.isAbsolute(filePath)
41
+ ? filePath
42
+ : path.resolve(process.cwd(), filePath);
43
+ let raw;
44
+ try {
45
+ raw = await fs.readFile(abs, "utf-8");
46
+ } catch (err) {
47
+ throw new Error(`loadConfig: could not read '${abs}': ${err.message}`);
48
+ }
49
+ let cfg;
50
+ try {
51
+ cfg = JSON.parse(raw);
52
+ } catch (err) {
53
+ throw new Error(`loadConfig: '${abs}' is not valid JSON: ${err.message}`);
54
+ }
55
+ validateConfig(cfg, abs);
56
+ if (typeof cfg.storagePath === "string" && cfg.storagePath.startsWith("~")) {
57
+ cfg.storagePath = cfg.storagePath.replace(/^~/, os.homedir());
58
+ }
59
+ return cfg;
60
+ }
61
+
62
+ /**
63
+ * Resolve the `capabilities` field of a config to an actual
64
+ * Array<McpCapability>. Accepts either a bare import path (resolved
65
+ * via dynamic import) or an inline array.
66
+ *
67
+ * @param {string | Array<object> | undefined} capabilities
68
+ * @returns {Promise<Array<object> | undefined>}
69
+ */
70
+ export async function resolveCapabilities(capabilities) {
71
+ if (capabilities === undefined || capabilities === null) return undefined;
72
+ if (Array.isArray(capabilities)) return capabilities;
73
+ if (typeof capabilities !== "string") {
74
+ throw new TypeError(
75
+ "resolveCapabilities: 'capabilities' must be a string import path or an array",
76
+ );
77
+ }
78
+ const mod = await import(capabilities);
79
+ // Look for the conventional name (foo → fooCapabilities, *Capabilities,
80
+ // default, or the first exported array). The companion packs use the
81
+ // `<server>Capabilities` convention, so check that first.
82
+ const exportedArrays = Object.entries(mod).filter(([, v]) => Array.isArray(v));
83
+ const conventional = exportedArrays.find(([k]) => k.endsWith("Capabilities"));
84
+ if (conventional) return conventional[1];
85
+ if (Array.isArray(mod.default)) return mod.default;
86
+ if (exportedArrays.length === 1) return exportedArrays[0][1];
87
+ throw new Error(
88
+ `resolveCapabilities: module '${capabilities}' does not export a recognisable capability array ` +
89
+ `(expected a *Capabilities export, a default export, or a single array export)`,
90
+ );
91
+ }
92
+
93
+ // ─── internal ─────────────────────────────────────────────────────────
94
+
95
+ function validateConfig(cfg, abs) {
96
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) {
97
+ throw new Error(`loadConfig: '${abs}' must be a JSON object`);
98
+ }
99
+ if (!cfg.upstream || typeof cfg.upstream !== "object") {
100
+ throw new Error(`loadConfig: '${abs}' is missing 'upstream' object`);
101
+ }
102
+ if (typeof cfg.upstream.command !== "string" || !cfg.upstream.command) {
103
+ throw new Error(`loadConfig: '${abs}' is missing 'upstream.command' string`);
104
+ }
105
+ if (cfg.upstream.args !== undefined && !Array.isArray(cfg.upstream.args)) {
106
+ throw new Error(`loadConfig: '${abs}' 'upstream.args' must be an array of strings if present`);
107
+ }
108
+ if (cfg.policy !== undefined && (typeof cfg.policy !== "object" || cfg.policy === null)) {
109
+ throw new Error(`loadConfig: '${abs}' 'policy' must be an object if present`);
110
+ }
111
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @strixgov/mcp-proxy
3
+ *
4
+ * Public API. Two entry points:
5
+ *
6
+ * - `startProxy(opts)` — programmatic. Used by the CLI; tests use it
7
+ * with `transport: { kind: "test", ... }`.
8
+ * - `loadConfig(path)` — read a proxy config JSON. CLI uses this;
9
+ * consumers can use it directly to validate a config before
10
+ * committing it to disk.
11
+ *
12
+ * Mode 3 (Capability-Enforced) is the v0.2.0 upgrade path. The seam
13
+ * is documented in src/proxy.mjs (`buildUpstreamIface`); the
14
+ * cryptographic primitive is already shipped at
15
+ * `@strixgov/mcp-token-validator`.
16
+ */
17
+
18
+ export { startProxy } from "./proxy.mjs";
19
+ export { loadConfig, resolveCapabilities } from "./config.mjs";