@prism-llm-labs/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/src/cli.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * mcp-proxy CLI
3
+ *
4
+ * Usage (everything after -- is the target command):
5
+ * mcp-proxy [options] -- <command> [args...]
6
+ *
7
+ * Examples:
8
+ * PRISM_API_KEY=prism_live_... mcp-proxy -- npx @modelcontextprotocol/server-filesystem /path
9
+ * mcp-proxy --prism-key prism_live_... -- node my-mcp-server.js
10
+ *
11
+ * Claude Desktop config:
12
+ * {
13
+ * "mcpServers": {
14
+ * "filesystem": {
15
+ * "command": "npx",
16
+ * "args": ["@prism-llm-labs/mcp-proxy", "--", "npx", "@modelcontextprotocol/server-filesystem", "/path"],
17
+ * "env": { "PRISM_API_KEY": "prism_live_..." }
18
+ * }
19
+ * }
20
+ * }
21
+ *
22
+ * Options:
23
+ * --prism-key <key> Prism API key (default: $PRISM_API_KEY)
24
+ * --server-name <name> Name shown in dashboard (default: target command basename)
25
+ * --project <id> Project ID for attribution
26
+ * --team <id> Team attribution tag
27
+ * --environment <env> production|staging|development (default: production)
28
+ * --session-id <id> Explicit session ID (default: auto-generated UUID)
29
+ * --session-budget <usd> Session budget in USD (e.g. 0.10)
30
+ * --max-tool-calls <n> Max tool/resource/prompt calls per session
31
+ * --capture-inputs Log call arguments to tags (opt-in, redacted)
32
+ * --capture-outputs Log call results to tags (opt-in, redacted)
33
+ * --ingest-url <url> Override Prism ingest URL (for self-hosted)
34
+ * --cost <tool=usd,...> Per-tool cost overrides, e.g. "run_bash=0.002"
35
+ */
36
+
37
+ import { PrismMcpProxy } from "./proxy.js";
38
+ import type { ProxyOptions } from "./types.js";
39
+
40
+ function usage(): void {
41
+ process.stderr.write(
42
+ "Usage: mcp-proxy [options] -- <command> [args...]\n" +
43
+ "\nOptions:\n" +
44
+ " --prism-key <key> Prism API key (default: $PRISM_API_KEY)\n" +
45
+ " --server-name <name> Name shown in dashboard\n" +
46
+ " --project <id> Project ID for attribution\n" +
47
+ " --team <id> Team attribution tag\n" +
48
+ " --environment <env> production|staging|development\n" +
49
+ " --session-id <id> Explicit session ID\n" +
50
+ " --session-budget <usd> Session budget in USD\n" +
51
+ " --max-tool-calls <n> Max calls per session\n" +
52
+ " --capture-inputs Log call arguments to tags\n" +
53
+ " --capture-outputs Log call results to tags\n" +
54
+ " --ingest-url <url> Override Prism ingest URL\n" +
55
+ " --cost <tool=usd,...> Per-tool cost overrides\n" +
56
+ "\nExample:\n" +
57
+ " mcp-proxy -- npx @modelcontextprotocol/server-filesystem /path/to/dir\n",
58
+ );
59
+ }
60
+
61
+ function parseArgs(argv: string[]): { target: string[]; opts: ProxyOptions } | null {
62
+ const args = argv.slice(2); // strip node + script path
63
+ const opts: ProxyOptions = {};
64
+ let i = 0;
65
+
66
+ // Find -- separator
67
+ const dashDash = args.indexOf("--");
68
+
69
+ if (dashDash === -1) {
70
+ process.stderr.write("[mcp-proxy] Error: missing -- separator before target command\n");
71
+ return null;
72
+ }
73
+
74
+ const target = args.slice(dashDash + 1);
75
+ if (target.length === 0) {
76
+ process.stderr.write("[mcp-proxy] Error: no target command after --\n");
77
+ return null;
78
+ }
79
+
80
+ // Parse options before --
81
+ const optArgs = args.slice(0, dashDash);
82
+ while (i < optArgs.length) {
83
+ const flag = optArgs[i]!;
84
+ switch (flag) {
85
+ case "--prism-key":
86
+ opts.prismKey = optArgs[++i];
87
+ break;
88
+ case "--server-name":
89
+ opts.serverName = optArgs[++i];
90
+ break;
91
+ case "--project":
92
+ opts.project = optArgs[++i];
93
+ break;
94
+ case "--team":
95
+ opts.team = optArgs[++i];
96
+ break;
97
+ case "--environment":
98
+ opts.environment = optArgs[++i];
99
+ break;
100
+ case "--session-id":
101
+ opts.sessionId = optArgs[++i];
102
+ break;
103
+ case "--session-budget": {
104
+ const budget = parseFloat(optArgs[++i] ?? "");
105
+ if (!isNaN(budget)) opts.sessionBudgetUsd = budget;
106
+ break;
107
+ }
108
+ case "--max-tool-calls": {
109
+ const max = parseInt(optArgs[++i] ?? "", 10);
110
+ if (!isNaN(max)) opts.maxToolCallsPerSession = max;
111
+ break;
112
+ }
113
+ case "--capture-inputs":
114
+ opts.captureInputs = true;
115
+ break;
116
+ case "--capture-outputs":
117
+ opts.captureOutputs = true;
118
+ break;
119
+ case "--ingest-url":
120
+ opts.ingestUrl = optArgs[++i];
121
+ break;
122
+ case "--cost": {
123
+ const overrides: Record<string, number> = {};
124
+ const pairs = (optArgs[++i] ?? "").split(",");
125
+ for (const pair of pairs) {
126
+ const [tool, cost] = pair.split("=");
127
+ if (tool && cost) {
128
+ const usd = parseFloat(cost);
129
+ if (!isNaN(usd)) overrides[tool] = usd;
130
+ }
131
+ }
132
+ opts.costOverrides = overrides;
133
+ break;
134
+ }
135
+ case "--help":
136
+ case "-h":
137
+ usage();
138
+ process.exit(0);
139
+ break;
140
+ default:
141
+ process.stderr.write(`[mcp-proxy] Unknown flag: ${flag}\n`);
142
+ return null;
143
+ }
144
+ i++;
145
+ }
146
+
147
+ return { target, opts };
148
+ }
149
+
150
+ async function main(): Promise<void> {
151
+ const parsed = parseArgs(process.argv);
152
+ if (!parsed) {
153
+ usage();
154
+ process.exit(1);
155
+ }
156
+
157
+ const [targetCommand, ...targetArgs] = parsed.target as [string, ...string[]];
158
+ const proxy = new PrismMcpProxy(targetCommand, targetArgs, parsed.opts);
159
+
160
+ // Graceful shutdown on SIGINT / SIGTERM
161
+ for (const sig of ["SIGINT", "SIGTERM"] as const) {
162
+ process.on(sig, () => process.exit(0));
163
+ }
164
+
165
+ try {
166
+ await proxy.run();
167
+ } catch (err) {
168
+ process.stderr.write(`[mcp-proxy] Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
169
+ process.exit(1);
170
+ }
171
+ }
172
+
173
+ main();
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { PrismMcpProxy } from "./proxy.js";
2
+ export type { ProxyOptions } from "./types.js";
package/src/proxy.ts ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * PrismMcpProxy — transparent process-level MCP proxy.
3
+ *
4
+ * Architecture:
5
+ * AI Client (Claude Desktop, Cline, etc.)
6
+ * ↕ MCP / stdio
7
+ * PrismMcpProxy ← this package
8
+ * ↕ MCP / stdio
9
+ * Target MCP server (any server, unmodified)
10
+ *
11
+ * The proxy:
12
+ * 1. Spawns the target as a child process via StdioClientTransport
13
+ * 2. Discovers what capabilities the target declares (tools / resources / prompts)
14
+ * 3. Creates a proxy Server that re-advertises those same capabilities
15
+ * 4. Intercepts every tool call, resource read, and prompt get:
16
+ * – checks session budget + loop limits (pre-call)
17
+ * – forwards the request to the target
18
+ * – measures wall-clock latency
19
+ * – ships a fire-and-forget McpEvent to /api/mcp/ingest
20
+ * – returns the target's response unchanged
21
+ * 5. Connects the proxy Server to the caller via StdioServerTransport
22
+ */
23
+
24
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
25
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
27
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
28
+ import {
29
+ CallToolRequestSchema,
30
+ GetPromptRequestSchema,
31
+ ListPromptsRequestSchema,
32
+ ListResourcesRequestSchema,
33
+ ListResourceTemplatesRequestSchema,
34
+ ListToolsRequestSchema,
35
+ ReadResourceRequestSchema,
36
+ type ServerCapabilities,
37
+ } from "@modelcontextprotocol/sdk/types.js";
38
+ import { McpEventTracker, SessionBudgetChecker, lookupToolCost } from "@prism-llm-labs/mcp-sdk";
39
+ import type { McpPrimitiveType } from "@prism-llm-labs/mcp-sdk";
40
+ import type { ProxyOptions } from "./types.js";
41
+
42
+ // ── Helpers ───────────────────────────────────────────────────────────────────
43
+
44
+ const DEFAULT_REDACT_KEYS = ["password", "token", "key", "secret", "api_key", "authorization"];
45
+
46
+ function redactObject(obj: unknown, keys: string[]): unknown {
47
+ if (typeof obj !== "object" || obj === null) return obj;
48
+ if (Array.isArray(obj)) return obj.map((v) => redactObject(v, keys));
49
+ const out: Record<string, unknown> = {};
50
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
51
+ out[k] = keys.some((r) => k.toLowerCase().includes(r.toLowerCase()))
52
+ ? "[REDACTED]"
53
+ : redactObject(v, keys);
54
+ }
55
+ return out;
56
+ }
57
+
58
+ function safeJson(val: unknown, redactKeys: string[], maxLen: number): string {
59
+ try {
60
+ const s = JSON.stringify(redactObject(val, redactKeys));
61
+ return s.length <= maxLen ? s : s.slice(0, maxLen) + "…";
62
+ } catch {
63
+ return "[unserializable]";
64
+ }
65
+ }
66
+
67
+ function orgFromKey(key: string): string {
68
+ const parts = key.split("_");
69
+ return parts.length >= 4 ? (parts[2] ?? "") : "";
70
+ }
71
+
72
+ function ts(): string {
73
+ return new Date().toISOString().replace("T", " ").slice(0, 23);
74
+ }
75
+
76
+ // ── Core class ────────────────────────────────────────────────────────────────
77
+
78
+ export class PrismMcpProxy {
79
+ private readonly opts: {
80
+ prismKey: string;
81
+ serverName: string;
82
+ project: string;
83
+ team: string;
84
+ environment: string;
85
+ sessionId: string;
86
+ sessionBudgetUsd?: number;
87
+ maxToolCallsPerSession?: number;
88
+ captureInputs: boolean;
89
+ captureOutputs: boolean;
90
+ costOverrides: Record<string, number>;
91
+ };
92
+ private readonly tracker: McpEventTracker;
93
+ private readonly budget: SessionBudgetChecker;
94
+ private readonly redactKeys: string[];
95
+
96
+ constructor(
97
+ private readonly targetCommand: string,
98
+ private readonly targetArgs: string[],
99
+ options: ProxyOptions = {},
100
+ ) {
101
+ const key = options.prismKey ?? process.env["PRISM_API_KEY"] ?? "";
102
+ if (!key) {
103
+ process.stderr.write("[prism-proxy] PRISM_API_KEY not set — telemetry disabled\n");
104
+ }
105
+
106
+ this.opts = {
107
+ prismKey: key,
108
+ serverName: options.serverName ?? targetCommand.split(/[\\/]/).pop() ?? "mcp-server",
109
+ project: options.project ?? process.env["PRISM_PROJECT"] ?? "",
110
+ team: options.team ?? process.env["PRISM_TEAM"] ?? "",
111
+ environment: options.environment ?? process.env["PRISM_ENVIRONMENT"] ?? "production",
112
+ sessionId: options.sessionId ?? crypto.randomUUID(),
113
+ sessionBudgetUsd: options.sessionBudgetUsd,
114
+ maxToolCallsPerSession: options.maxToolCallsPerSession,
115
+ captureInputs: options.captureInputs ?? false,
116
+ captureOutputs: options.captureOutputs ?? false,
117
+ costOverrides: options.costOverrides ?? {},
118
+ };
119
+
120
+ this.redactKeys = options.redactKeys ?? DEFAULT_REDACT_KEYS;
121
+ this.tracker = new McpEventTracker(key, this.opts.serverName, options.ingestUrl);
122
+ this.budget = new SessionBudgetChecker(orgFromKey(key));
123
+ }
124
+
125
+ /**
126
+ * Start the proxy. Blocks until the AI client disconnects.
127
+ * Spawn the target server, connect both transports, then wait.
128
+ */
129
+ async run(): Promise<void> {
130
+ // ── 1. Connect to target server ──────────────────────────────────────────
131
+ const targetTransport = new StdioClientTransport({
132
+ command: this.targetCommand,
133
+ args: this.targetArgs,
134
+ stderr: "pipe", // don't let target's stderr pollute our stdout
135
+ });
136
+
137
+ const targetClient = new Client(
138
+ { name: "prism-proxy-client", version: "1.0.0" },
139
+ { capabilities: {} },
140
+ );
141
+
142
+ await targetClient.connect(targetTransport);
143
+
144
+ const caps: ServerCapabilities = targetClient.getServerCapabilities() ?? {};
145
+ const hasTools = !!caps.tools;
146
+ const hasResources = !!caps.resources;
147
+ const hasPrompts = !!caps.prompts;
148
+
149
+ // ── 2. Build proxy server ────────────────────────────────────────────────
150
+ const proxyCaps: ServerCapabilities = {};
151
+ if (hasTools) proxyCaps.tools = {};
152
+ if (hasResources) proxyCaps.resources = { listChanged: false, subscribe: false };
153
+ if (hasPrompts) proxyCaps.prompts = { listChanged: false };
154
+
155
+ const proxyServer = new Server(
156
+ { name: this.opts.serverName, version: "1.0.0" },
157
+ { capabilities: proxyCaps },
158
+ );
159
+
160
+ // ── 3. Register handlers ─────────────────────────────────────────────────
161
+
162
+ if (hasTools) {
163
+ proxyServer.setRequestHandler(
164
+ ListToolsRequestSchema,
165
+ () => targetClient.listTools(),
166
+ );
167
+
168
+ proxyServer.setRequestHandler(
169
+ CallToolRequestSchema,
170
+ (req) => this._handleToolCall(req.params.name, req.params.arguments ?? {}, targetClient),
171
+ );
172
+ }
173
+
174
+ if (hasResources) {
175
+ proxyServer.setRequestHandler(
176
+ ListResourcesRequestSchema,
177
+ () => targetClient.listResources(),
178
+ );
179
+
180
+ // Resource templates — not all servers support this; ignore if unavailable
181
+ proxyServer.setRequestHandler(
182
+ ListResourceTemplatesRequestSchema,
183
+ async () => {
184
+ try {
185
+ return await targetClient.listResourceTemplates();
186
+ } catch {
187
+ return { resourceTemplates: [] };
188
+ }
189
+ },
190
+ );
191
+
192
+ proxyServer.setRequestHandler(
193
+ ReadResourceRequestSchema,
194
+ (req) => this._handleResourceRead(req.params.uri, targetClient),
195
+ );
196
+ }
197
+
198
+ if (hasPrompts) {
199
+ proxyServer.setRequestHandler(
200
+ ListPromptsRequestSchema,
201
+ () => targetClient.listPrompts(),
202
+ );
203
+
204
+ proxyServer.setRequestHandler(
205
+ GetPromptRequestSchema,
206
+ (req) => this._handlePromptGet(
207
+ req.params.name,
208
+ req.params.arguments as Record<string, string> | undefined,
209
+ targetClient,
210
+ ),
211
+ );
212
+ }
213
+
214
+ // ── 4. Connect proxy to the AI client via stdio ──────────────────────────
215
+ const proxyTransport = new StdioServerTransport();
216
+ await proxyServer.connect(proxyTransport);
217
+
218
+ // ── 5. Wait for disconnect ───────────────────────────────────────────────
219
+ await new Promise<void>((resolve) => {
220
+ proxyTransport.onclose = resolve;
221
+ });
222
+
223
+ try { await targetClient.close(); } catch { /* ignore */ }
224
+ }
225
+
226
+ // ── Private: tool call intercept ──────────────────────────────────────────
227
+
228
+ private async _handleToolCall(
229
+ name: string,
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
231
+ args: Record<string, any>,
232
+ target: Client,
233
+ ) {
234
+ await this._checkBudget();
235
+
236
+ const estimatedCost = lookupToolCost(name, this.opts.costOverrides);
237
+ const start = Date.now();
238
+ const eventTags: Record<string, string> = {};
239
+
240
+ if (this.opts.captureInputs) {
241
+ eventTags["tool_input"] = safeJson(args, this.redactKeys, 1000);
242
+ }
243
+
244
+ let status: "ok" | "error" | "timeout" = "ok";
245
+ let errorMsg = "";
246
+
247
+ let result: Awaited<ReturnType<Client["callTool"]>>;
248
+ try {
249
+ result = await target.callTool({ name, arguments: args });
250
+ } catch (err) {
251
+ status = "error";
252
+ errorMsg = err instanceof Error ? err.message : String(err);
253
+ this._ship("tool", name, Date.now() - start, estimatedCost, "estimated", status, errorMsg, eventTags);
254
+ throw err;
255
+ }
256
+
257
+ const latencyMs = Date.now() - start;
258
+
259
+ // MCP tool errors are returned in the result (not thrown) with isError=true
260
+ if (result.isError) {
261
+ status = "error";
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ const content = result.content as any[];
264
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
+ const errBlock = Array.isArray(content) ? content.find((c: any) => c?.type === "text") : null;
266
+ errorMsg = (errBlock as { text?: string } | null)?.text ?? "Tool returned error";
267
+ }
268
+
269
+ if (this.opts.captureOutputs) {
270
+ eventTags["tool_output"] = safeJson(result.content, this.redactKeys, 1000);
271
+ }
272
+
273
+ this._ship("tool", name, latencyMs, estimatedCost, "estimated", status, errorMsg, eventTags);
274
+ return result;
275
+ }
276
+
277
+ // ── Private: resource read intercept ─────────────────────────────────────
278
+
279
+ private async _handleResourceRead(uri: string, target: Client) {
280
+ await this._checkBudget();
281
+
282
+ const start = Date.now();
283
+ let status: "ok" | "error" | "timeout" = "ok";
284
+ let errorMsg = "";
285
+
286
+ let result: Awaited<ReturnType<Client["readResource"]>>;
287
+ try {
288
+ result = await target.readResource({ uri });
289
+ } catch (err) {
290
+ status = "error";
291
+ errorMsg = err instanceof Error ? err.message : String(err);
292
+ this._ship("resource", uri, Date.now() - start, 0, "estimated", status, errorMsg, {});
293
+ throw err;
294
+ }
295
+
296
+ this._ship("resource", uri, Date.now() - start, 0, "estimated", status, errorMsg, {});
297
+ return result;
298
+ }
299
+
300
+ // ── Private: prompt get intercept ────────────────────────────────────────
301
+
302
+ private async _handlePromptGet(
303
+ name: string,
304
+ args: Record<string, string> | undefined,
305
+ target: Client,
306
+ ) {
307
+ await this._checkBudget();
308
+
309
+ const start = Date.now();
310
+ let status: "ok" | "error" | "timeout" = "ok";
311
+ let errorMsg = "";
312
+
313
+ let result: Awaited<ReturnType<Client["getPrompt"]>>;
314
+ try {
315
+ result = await target.getPrompt({ name, arguments: args });
316
+ } catch (err) {
317
+ status = "error";
318
+ errorMsg = err instanceof Error ? err.message : String(err);
319
+ this._ship("prompt", name, Date.now() - start, 0, "estimated", status, errorMsg, {});
320
+ throw err;
321
+ }
322
+
323
+ this._ship("prompt", name, Date.now() - start, 0, "estimated", status, errorMsg, {});
324
+ return result;
325
+ }
326
+
327
+ // ── Private: shared helpers ───────────────────────────────────────────────
328
+
329
+ private async _checkBudget(): Promise<void> {
330
+ await this.budget.checkOrThrow(
331
+ this.opts.sessionId,
332
+ this.opts.sessionBudgetUsd,
333
+ this.opts.maxToolCallsPerSession,
334
+ );
335
+ }
336
+
337
+ private _ship(
338
+ primitiveType: McpPrimitiveType,
339
+ toolName: string,
340
+ latencyMs: number,
341
+ costUsd: number,
342
+ costStatus: "estimated" | "actual",
343
+ status: "ok" | "error" | "timeout",
344
+ errorMessage: string,
345
+ tags: Record<string, string>,
346
+ ): void {
347
+ this.tracker.capture({
348
+ timestamp: ts(),
349
+ session_id: this.opts.sessionId,
350
+ project_id: this.opts.project,
351
+ team_id: this.opts.team,
352
+ user_id: "",
353
+ environment: this.opts.environment,
354
+ tool_name: toolName,
355
+ downstream_resource: "",
356
+ execution_latency_ms: latencyMs,
357
+ tool_cost_usd: costUsd,
358
+ cost_status: costStatus,
359
+ status,
360
+ error_message: errorMessage,
361
+ llm_request_id: "",
362
+ primitive_type: primitiveType,
363
+ tags,
364
+ }).catch(() => {});
365
+ }
366
+ }
package/src/types.ts ADDED
@@ -0,0 +1,49 @@
1
+ export interface ProxyOptions {
2
+ /** Prism API key — or set PRISM_API_KEY env var */
3
+ prismKey?: string;
4
+ /**
5
+ * Name shown in the Prism dashboard as the MCP server name.
6
+ * Defaults to the basename of the target command.
7
+ */
8
+ serverName?: string;
9
+ /** Project ID for cost attribution */
10
+ project?: string;
11
+ /** Team attribution tag */
12
+ team?: string;
13
+ /** "production" | "staging" | "development" (default: "production") */
14
+ environment?: string;
15
+ /**
16
+ * Explicit session ID. Auto-generated UUID if omitted.
17
+ * One proxy process = one session = one agent run.
18
+ */
19
+ sessionId?: string;
20
+ /**
21
+ * Session budget in USD. All tool/resource/prompt calls are blocked
22
+ * when combined session cost exceeds this value.
23
+ */
24
+ sessionBudgetUsd?: number;
25
+ /**
26
+ * Maximum MCP primitive calls per session.
27
+ * Blocks further calls when exceeded — loop detection guard.
28
+ */
29
+ maxToolCallsPerSession?: number;
30
+ /**
31
+ * Log call arguments into tags["tool_input"] (truncated to 1000 chars).
32
+ * Opt-in only — disabled by default for privacy.
33
+ */
34
+ captureInputs?: boolean;
35
+ /**
36
+ * Log call results into tags["tool_output"] (truncated to 1000 chars).
37
+ * Opt-in only — disabled by default for privacy.
38
+ */
39
+ captureOutputs?: boolean;
40
+ /**
41
+ * Keys to redact from captured inputs/outputs.
42
+ * Default: ["password", "token", "key", "secret", "api_key", "authorization"]
43
+ */
44
+ redactKeys?: string[];
45
+ /** Override ingest URL (for testing / self-hosted Prism) */
46
+ ingestUrl?: string;
47
+ /** Per-tool cost overrides in USD per call (tool_name → usd) */
48
+ costOverrides?: Record<string, number>;
49
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { PrismMcpProxy } from "../src/proxy.js";
3
+
4
+ describe("PrismMcpProxy", () => {
5
+ it("constructs without throwing when no key is set", () => {
6
+ // Should warn but not throw — observability must never crash the app
7
+ const proxy = new PrismMcpProxy("echo", ["hello"], {
8
+ prismKey: "",
9
+ serverName: "test-server",
10
+ project: "proj-1",
11
+ });
12
+ expect(proxy).toBeDefined();
13
+ });
14
+
15
+ it("derives server name from target command basename", () => {
16
+ // serverName should default to the last segment of the command path
17
+ const proxy = new PrismMcpProxy(
18
+ "/usr/local/bin/my-mcp-server",
19
+ [],
20
+ { prismKey: "prism_live_abc_def" },
21
+ );
22
+ // Access via cast to test private field
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ expect((proxy as any).opts.serverName).toBe("my-mcp-server");
25
+ });
26
+
27
+ it("accepts all option overrides", () => {
28
+ const proxy = new PrismMcpProxy("cmd", [], {
29
+ prismKey: "prism_live_abc_def",
30
+ serverName: "my-server",
31
+ project: "proj",
32
+ team: "team-a",
33
+ environment: "staging",
34
+ sessionId: "sess-123",
35
+ sessionBudgetUsd: 0.50,
36
+ maxToolCallsPerSession: 100,
37
+ captureInputs: true,
38
+ captureOutputs: true,
39
+ costOverrides: { bash: 0.005 },
40
+ ingestUrl: "https://example.com/api/mcp/ingest",
41
+ });
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const opts = (proxy as any).opts;
45
+ expect(opts.serverName).toBe("my-server");
46
+ expect(opts.sessionBudgetUsd).toBe(0.50);
47
+ expect(opts.maxToolCallsPerSession).toBe(100);
48
+ expect(opts.captureInputs).toBe(true);
49
+ expect(opts.costOverrides).toEqual({ bash: 0.005 });
50
+ });
51
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2020"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "outDir": "./dist",
13
+ "rootDir": "./src"
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist", "tests"]
17
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig([
4
+ // Library entry — consumed programmatically
5
+ {
6
+ entry: ["src/index.ts"],
7
+ format: ["cjs", "esm"],
8
+ dts: true,
9
+ clean: true,
10
+ sourcemap: true,
11
+ external: ["@modelcontextprotocol/sdk"],
12
+ },
13
+ // CLI entry — executable bin; shebang added via banner
14
+ {
15
+ entry: { cli: "src/cli.ts" },
16
+ format: ["cjs"],
17
+ dts: false,
18
+ clean: false,
19
+ sourcemap: false,
20
+ external: ["@modelcontextprotocol/sdk"],
21
+ banner: { js: "#!/usr/bin/env node" },
22
+ },
23
+ ]);