@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/dist/cli.js +350 -0
- package/dist/index.d.mts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +269 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +250 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
- package/src/cli.ts +173 -0
- package/src/index.ts +2 -0
- package/src/proxy.ts +366 -0
- package/src/types.ts +49 -0
- package/tests/proxy.test.ts +51 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +23 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/proxy.ts
|
|
5
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
6
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
8
|
+
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
9
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
10
|
+
var import_mcp_sdk = require("@prism-llm-labs/mcp-sdk");
|
|
11
|
+
var DEFAULT_REDACT_KEYS = ["password", "token", "key", "secret", "api_key", "authorization"];
|
|
12
|
+
function redactObject(obj, keys) {
|
|
13
|
+
if (typeof obj !== "object" || obj === null) return obj;
|
|
14
|
+
if (Array.isArray(obj)) return obj.map((v) => redactObject(v, keys));
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
17
|
+
out[k] = keys.some((r) => k.toLowerCase().includes(r.toLowerCase())) ? "[REDACTED]" : redactObject(v, keys);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function safeJson(val, redactKeys, maxLen) {
|
|
22
|
+
try {
|
|
23
|
+
const s = JSON.stringify(redactObject(val, redactKeys));
|
|
24
|
+
return s.length <= maxLen ? s : s.slice(0, maxLen) + "\u2026";
|
|
25
|
+
} catch {
|
|
26
|
+
return "[unserializable]";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function orgFromKey(key) {
|
|
30
|
+
const parts = key.split("_");
|
|
31
|
+
return parts.length >= 4 ? parts[2] ?? "" : "";
|
|
32
|
+
}
|
|
33
|
+
function ts() {
|
|
34
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 23);
|
|
35
|
+
}
|
|
36
|
+
var PrismMcpProxy = class {
|
|
37
|
+
constructor(targetCommand, targetArgs, options = {}) {
|
|
38
|
+
this.targetCommand = targetCommand;
|
|
39
|
+
this.targetArgs = targetArgs;
|
|
40
|
+
const key = options.prismKey ?? process.env["PRISM_API_KEY"] ?? "";
|
|
41
|
+
if (!key) {
|
|
42
|
+
process.stderr.write("[prism-proxy] PRISM_API_KEY not set \u2014 telemetry disabled\n");
|
|
43
|
+
}
|
|
44
|
+
this.opts = {
|
|
45
|
+
prismKey: key,
|
|
46
|
+
serverName: options.serverName ?? targetCommand.split(/[\\/]/).pop() ?? "mcp-server",
|
|
47
|
+
project: options.project ?? process.env["PRISM_PROJECT"] ?? "",
|
|
48
|
+
team: options.team ?? process.env["PRISM_TEAM"] ?? "",
|
|
49
|
+
environment: options.environment ?? process.env["PRISM_ENVIRONMENT"] ?? "production",
|
|
50
|
+
sessionId: options.sessionId ?? crypto.randomUUID(),
|
|
51
|
+
sessionBudgetUsd: options.sessionBudgetUsd,
|
|
52
|
+
maxToolCallsPerSession: options.maxToolCallsPerSession,
|
|
53
|
+
captureInputs: options.captureInputs ?? false,
|
|
54
|
+
captureOutputs: options.captureOutputs ?? false,
|
|
55
|
+
costOverrides: options.costOverrides ?? {}
|
|
56
|
+
};
|
|
57
|
+
this.redactKeys = options.redactKeys ?? DEFAULT_REDACT_KEYS;
|
|
58
|
+
this.tracker = new import_mcp_sdk.McpEventTracker(key, this.opts.serverName, options.ingestUrl);
|
|
59
|
+
this.budget = new import_mcp_sdk.SessionBudgetChecker(orgFromKey(key));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Start the proxy. Blocks until the AI client disconnects.
|
|
63
|
+
* Spawn the target server, connect both transports, then wait.
|
|
64
|
+
*/
|
|
65
|
+
async run() {
|
|
66
|
+
const targetTransport = new import_stdio2.StdioClientTransport({
|
|
67
|
+
command: this.targetCommand,
|
|
68
|
+
args: this.targetArgs,
|
|
69
|
+
stderr: "pipe"
|
|
70
|
+
// don't let target's stderr pollute our stdout
|
|
71
|
+
});
|
|
72
|
+
const targetClient = new import_client.Client(
|
|
73
|
+
{ name: "prism-proxy-client", version: "1.0.0" },
|
|
74
|
+
{ capabilities: {} }
|
|
75
|
+
);
|
|
76
|
+
await targetClient.connect(targetTransport);
|
|
77
|
+
const caps = targetClient.getServerCapabilities() ?? {};
|
|
78
|
+
const hasTools = !!caps.tools;
|
|
79
|
+
const hasResources = !!caps.resources;
|
|
80
|
+
const hasPrompts = !!caps.prompts;
|
|
81
|
+
const proxyCaps = {};
|
|
82
|
+
if (hasTools) proxyCaps.tools = {};
|
|
83
|
+
if (hasResources) proxyCaps.resources = { listChanged: false, subscribe: false };
|
|
84
|
+
if (hasPrompts) proxyCaps.prompts = { listChanged: false };
|
|
85
|
+
const proxyServer = new import_server.Server(
|
|
86
|
+
{ name: this.opts.serverName, version: "1.0.0" },
|
|
87
|
+
{ capabilities: proxyCaps }
|
|
88
|
+
);
|
|
89
|
+
if (hasTools) {
|
|
90
|
+
proxyServer.setRequestHandler(
|
|
91
|
+
import_types.ListToolsRequestSchema,
|
|
92
|
+
() => targetClient.listTools()
|
|
93
|
+
);
|
|
94
|
+
proxyServer.setRequestHandler(
|
|
95
|
+
import_types.CallToolRequestSchema,
|
|
96
|
+
(req) => this._handleToolCall(req.params.name, req.params.arguments ?? {}, targetClient)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (hasResources) {
|
|
100
|
+
proxyServer.setRequestHandler(
|
|
101
|
+
import_types.ListResourcesRequestSchema,
|
|
102
|
+
() => targetClient.listResources()
|
|
103
|
+
);
|
|
104
|
+
proxyServer.setRequestHandler(
|
|
105
|
+
import_types.ListResourceTemplatesRequestSchema,
|
|
106
|
+
async () => {
|
|
107
|
+
try {
|
|
108
|
+
return await targetClient.listResourceTemplates();
|
|
109
|
+
} catch {
|
|
110
|
+
return { resourceTemplates: [] };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
proxyServer.setRequestHandler(
|
|
115
|
+
import_types.ReadResourceRequestSchema,
|
|
116
|
+
(req) => this._handleResourceRead(req.params.uri, targetClient)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (hasPrompts) {
|
|
120
|
+
proxyServer.setRequestHandler(
|
|
121
|
+
import_types.ListPromptsRequestSchema,
|
|
122
|
+
() => targetClient.listPrompts()
|
|
123
|
+
);
|
|
124
|
+
proxyServer.setRequestHandler(
|
|
125
|
+
import_types.GetPromptRequestSchema,
|
|
126
|
+
(req) => this._handlePromptGet(
|
|
127
|
+
req.params.name,
|
|
128
|
+
req.params.arguments,
|
|
129
|
+
targetClient
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const proxyTransport = new import_stdio.StdioServerTransport();
|
|
134
|
+
await proxyServer.connect(proxyTransport);
|
|
135
|
+
await new Promise((resolve) => {
|
|
136
|
+
proxyTransport.onclose = resolve;
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
await targetClient.close();
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── Private: tool call intercept ──────────────────────────────────────────
|
|
144
|
+
async _handleToolCall(name, args, target) {
|
|
145
|
+
await this._checkBudget();
|
|
146
|
+
const estimatedCost = (0, import_mcp_sdk.lookupToolCost)(name, this.opts.costOverrides);
|
|
147
|
+
const start = Date.now();
|
|
148
|
+
const eventTags = {};
|
|
149
|
+
if (this.opts.captureInputs) {
|
|
150
|
+
eventTags["tool_input"] = safeJson(args, this.redactKeys, 1e3);
|
|
151
|
+
}
|
|
152
|
+
let status = "ok";
|
|
153
|
+
let errorMsg = "";
|
|
154
|
+
let result;
|
|
155
|
+
try {
|
|
156
|
+
result = await target.callTool({ name, arguments: args });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
status = "error";
|
|
159
|
+
errorMsg = err instanceof Error ? err.message : String(err);
|
|
160
|
+
this._ship("tool", name, Date.now() - start, estimatedCost, "estimated", status, errorMsg, eventTags);
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
const latencyMs = Date.now() - start;
|
|
164
|
+
if (result.isError) {
|
|
165
|
+
status = "error";
|
|
166
|
+
const content = result.content;
|
|
167
|
+
const errBlock = Array.isArray(content) ? content.find((c) => c?.type === "text") : null;
|
|
168
|
+
errorMsg = errBlock?.text ?? "Tool returned error";
|
|
169
|
+
}
|
|
170
|
+
if (this.opts.captureOutputs) {
|
|
171
|
+
eventTags["tool_output"] = safeJson(result.content, this.redactKeys, 1e3);
|
|
172
|
+
}
|
|
173
|
+
this._ship("tool", name, latencyMs, estimatedCost, "estimated", status, errorMsg, eventTags);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
// ── Private: resource read intercept ─────────────────────────────────────
|
|
177
|
+
async _handleResourceRead(uri, target) {
|
|
178
|
+
await this._checkBudget();
|
|
179
|
+
const start = Date.now();
|
|
180
|
+
let status = "ok";
|
|
181
|
+
let errorMsg = "";
|
|
182
|
+
let result;
|
|
183
|
+
try {
|
|
184
|
+
result = await target.readResource({ uri });
|
|
185
|
+
} catch (err) {
|
|
186
|
+
status = "error";
|
|
187
|
+
errorMsg = err instanceof Error ? err.message : String(err);
|
|
188
|
+
this._ship("resource", uri, Date.now() - start, 0, "estimated", status, errorMsg, {});
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
this._ship("resource", uri, Date.now() - start, 0, "estimated", status, errorMsg, {});
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
// ── Private: prompt get intercept ────────────────────────────────────────
|
|
195
|
+
async _handlePromptGet(name, args, target) {
|
|
196
|
+
await this._checkBudget();
|
|
197
|
+
const start = Date.now();
|
|
198
|
+
let status = "ok";
|
|
199
|
+
let errorMsg = "";
|
|
200
|
+
let result;
|
|
201
|
+
try {
|
|
202
|
+
result = await target.getPrompt({ name, arguments: args });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
status = "error";
|
|
205
|
+
errorMsg = err instanceof Error ? err.message : String(err);
|
|
206
|
+
this._ship("prompt", name, Date.now() - start, 0, "estimated", status, errorMsg, {});
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
this._ship("prompt", name, Date.now() - start, 0, "estimated", status, errorMsg, {});
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
// ── Private: shared helpers ───────────────────────────────────────────────
|
|
213
|
+
async _checkBudget() {
|
|
214
|
+
await this.budget.checkOrThrow(
|
|
215
|
+
this.opts.sessionId,
|
|
216
|
+
this.opts.sessionBudgetUsd,
|
|
217
|
+
this.opts.maxToolCallsPerSession
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
_ship(primitiveType, toolName, latencyMs, costUsd, costStatus, status, errorMessage, tags) {
|
|
221
|
+
this.tracker.capture({
|
|
222
|
+
timestamp: ts(),
|
|
223
|
+
session_id: this.opts.sessionId,
|
|
224
|
+
project_id: this.opts.project,
|
|
225
|
+
team_id: this.opts.team,
|
|
226
|
+
user_id: "",
|
|
227
|
+
environment: this.opts.environment,
|
|
228
|
+
tool_name: toolName,
|
|
229
|
+
downstream_resource: "",
|
|
230
|
+
execution_latency_ms: latencyMs,
|
|
231
|
+
tool_cost_usd: costUsd,
|
|
232
|
+
cost_status: costStatus,
|
|
233
|
+
status,
|
|
234
|
+
error_message: errorMessage,
|
|
235
|
+
llm_request_id: "",
|
|
236
|
+
primitive_type: primitiveType,
|
|
237
|
+
tags
|
|
238
|
+
}).catch(() => {
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/cli.ts
|
|
244
|
+
function usage() {
|
|
245
|
+
process.stderr.write(
|
|
246
|
+
"Usage: mcp-proxy [options] -- <command> [args...]\n\nOptions:\n --prism-key <key> Prism API key (default: $PRISM_API_KEY)\n --server-name <name> Name shown in dashboard\n --project <id> Project ID for attribution\n --team <id> Team attribution tag\n --environment <env> production|staging|development\n --session-id <id> Explicit session ID\n --session-budget <usd> Session budget in USD\n --max-tool-calls <n> Max calls per session\n --capture-inputs Log call arguments to tags\n --capture-outputs Log call results to tags\n --ingest-url <url> Override Prism ingest URL\n --cost <tool=usd,...> Per-tool cost overrides\n\nExample:\n mcp-proxy -- npx @modelcontextprotocol/server-filesystem /path/to/dir\n"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
function parseArgs(argv) {
|
|
250
|
+
const args = argv.slice(2);
|
|
251
|
+
const opts = {};
|
|
252
|
+
let i = 0;
|
|
253
|
+
const dashDash = args.indexOf("--");
|
|
254
|
+
if (dashDash === -1) {
|
|
255
|
+
process.stderr.write("[mcp-proxy] Error: missing -- separator before target command\n");
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const target = args.slice(dashDash + 1);
|
|
259
|
+
if (target.length === 0) {
|
|
260
|
+
process.stderr.write("[mcp-proxy] Error: no target command after --\n");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const optArgs = args.slice(0, dashDash);
|
|
264
|
+
while (i < optArgs.length) {
|
|
265
|
+
const flag = optArgs[i];
|
|
266
|
+
switch (flag) {
|
|
267
|
+
case "--prism-key":
|
|
268
|
+
opts.prismKey = optArgs[++i];
|
|
269
|
+
break;
|
|
270
|
+
case "--server-name":
|
|
271
|
+
opts.serverName = optArgs[++i];
|
|
272
|
+
break;
|
|
273
|
+
case "--project":
|
|
274
|
+
opts.project = optArgs[++i];
|
|
275
|
+
break;
|
|
276
|
+
case "--team":
|
|
277
|
+
opts.team = optArgs[++i];
|
|
278
|
+
break;
|
|
279
|
+
case "--environment":
|
|
280
|
+
opts.environment = optArgs[++i];
|
|
281
|
+
break;
|
|
282
|
+
case "--session-id":
|
|
283
|
+
opts.sessionId = optArgs[++i];
|
|
284
|
+
break;
|
|
285
|
+
case "--session-budget": {
|
|
286
|
+
const budget = parseFloat(optArgs[++i] ?? "");
|
|
287
|
+
if (!isNaN(budget)) opts.sessionBudgetUsd = budget;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "--max-tool-calls": {
|
|
291
|
+
const max = parseInt(optArgs[++i] ?? "", 10);
|
|
292
|
+
if (!isNaN(max)) opts.maxToolCallsPerSession = max;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case "--capture-inputs":
|
|
296
|
+
opts.captureInputs = true;
|
|
297
|
+
break;
|
|
298
|
+
case "--capture-outputs":
|
|
299
|
+
opts.captureOutputs = true;
|
|
300
|
+
break;
|
|
301
|
+
case "--ingest-url":
|
|
302
|
+
opts.ingestUrl = optArgs[++i];
|
|
303
|
+
break;
|
|
304
|
+
case "--cost": {
|
|
305
|
+
const overrides = {};
|
|
306
|
+
const pairs = (optArgs[++i] ?? "").split(",");
|
|
307
|
+
for (const pair of pairs) {
|
|
308
|
+
const [tool, cost] = pair.split("=");
|
|
309
|
+
if (tool && cost) {
|
|
310
|
+
const usd = parseFloat(cost);
|
|
311
|
+
if (!isNaN(usd)) overrides[tool] = usd;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
opts.costOverrides = overrides;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "--help":
|
|
318
|
+
case "-h":
|
|
319
|
+
usage();
|
|
320
|
+
process.exit(0);
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
process.stderr.write(`[mcp-proxy] Unknown flag: ${flag}
|
|
324
|
+
`);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
i++;
|
|
328
|
+
}
|
|
329
|
+
return { target, opts };
|
|
330
|
+
}
|
|
331
|
+
async function main() {
|
|
332
|
+
const parsed = parseArgs(process.argv);
|
|
333
|
+
if (!parsed) {
|
|
334
|
+
usage();
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const [targetCommand, ...targetArgs] = parsed.target;
|
|
338
|
+
const proxy = new PrismMcpProxy(targetCommand, targetArgs, parsed.opts);
|
|
339
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
340
|
+
process.on(sig, () => process.exit(0));
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
await proxy.run();
|
|
344
|
+
} catch (err) {
|
|
345
|
+
process.stderr.write(`[mcp-proxy] Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
346
|
+
`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
main();
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
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
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* PrismMcpProxy — transparent process-level MCP proxy.
|
|
53
|
+
*
|
|
54
|
+
* Architecture:
|
|
55
|
+
* AI Client (Claude Desktop, Cline, etc.)
|
|
56
|
+
* ↕ MCP / stdio
|
|
57
|
+
* PrismMcpProxy ← this package
|
|
58
|
+
* ↕ MCP / stdio
|
|
59
|
+
* Target MCP server (any server, unmodified)
|
|
60
|
+
*
|
|
61
|
+
* The proxy:
|
|
62
|
+
* 1. Spawns the target as a child process via StdioClientTransport
|
|
63
|
+
* 2. Discovers what capabilities the target declares (tools / resources / prompts)
|
|
64
|
+
* 3. Creates a proxy Server that re-advertises those same capabilities
|
|
65
|
+
* 4. Intercepts every tool call, resource read, and prompt get:
|
|
66
|
+
* – checks session budget + loop limits (pre-call)
|
|
67
|
+
* – forwards the request to the target
|
|
68
|
+
* – measures wall-clock latency
|
|
69
|
+
* – ships a fire-and-forget McpEvent to /api/mcp/ingest
|
|
70
|
+
* – returns the target's response unchanged
|
|
71
|
+
* 5. Connects the proxy Server to the caller via StdioServerTransport
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
declare class PrismMcpProxy {
|
|
75
|
+
private readonly targetCommand;
|
|
76
|
+
private readonly targetArgs;
|
|
77
|
+
private readonly opts;
|
|
78
|
+
private readonly tracker;
|
|
79
|
+
private readonly budget;
|
|
80
|
+
private readonly redactKeys;
|
|
81
|
+
constructor(targetCommand: string, targetArgs: string[], options?: ProxyOptions);
|
|
82
|
+
/**
|
|
83
|
+
* Start the proxy. Blocks until the AI client disconnects.
|
|
84
|
+
* Spawn the target server, connect both transports, then wait.
|
|
85
|
+
*/
|
|
86
|
+
run(): Promise<void>;
|
|
87
|
+
private _handleToolCall;
|
|
88
|
+
private _handleResourceRead;
|
|
89
|
+
private _handlePromptGet;
|
|
90
|
+
private _checkBudget;
|
|
91
|
+
private _ship;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { PrismMcpProxy, type ProxyOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
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
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* PrismMcpProxy — transparent process-level MCP proxy.
|
|
53
|
+
*
|
|
54
|
+
* Architecture:
|
|
55
|
+
* AI Client (Claude Desktop, Cline, etc.)
|
|
56
|
+
* ↕ MCP / stdio
|
|
57
|
+
* PrismMcpProxy ← this package
|
|
58
|
+
* ↕ MCP / stdio
|
|
59
|
+
* Target MCP server (any server, unmodified)
|
|
60
|
+
*
|
|
61
|
+
* The proxy:
|
|
62
|
+
* 1. Spawns the target as a child process via StdioClientTransport
|
|
63
|
+
* 2. Discovers what capabilities the target declares (tools / resources / prompts)
|
|
64
|
+
* 3. Creates a proxy Server that re-advertises those same capabilities
|
|
65
|
+
* 4. Intercepts every tool call, resource read, and prompt get:
|
|
66
|
+
* – checks session budget + loop limits (pre-call)
|
|
67
|
+
* – forwards the request to the target
|
|
68
|
+
* – measures wall-clock latency
|
|
69
|
+
* – ships a fire-and-forget McpEvent to /api/mcp/ingest
|
|
70
|
+
* – returns the target's response unchanged
|
|
71
|
+
* 5. Connects the proxy Server to the caller via StdioServerTransport
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
declare class PrismMcpProxy {
|
|
75
|
+
private readonly targetCommand;
|
|
76
|
+
private readonly targetArgs;
|
|
77
|
+
private readonly opts;
|
|
78
|
+
private readonly tracker;
|
|
79
|
+
private readonly budget;
|
|
80
|
+
private readonly redactKeys;
|
|
81
|
+
constructor(targetCommand: string, targetArgs: string[], options?: ProxyOptions);
|
|
82
|
+
/**
|
|
83
|
+
* Start the proxy. Blocks until the AI client disconnects.
|
|
84
|
+
* Spawn the target server, connect both transports, then wait.
|
|
85
|
+
*/
|
|
86
|
+
run(): Promise<void>;
|
|
87
|
+
private _handleToolCall;
|
|
88
|
+
private _handleResourceRead;
|
|
89
|
+
private _handlePromptGet;
|
|
90
|
+
private _checkBudget;
|
|
91
|
+
private _ship;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { PrismMcpProxy, type ProxyOptions };
|