@prism-llm-labs/mcp-sdk 0.2.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/index.d.mts +310 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +448 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +414 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +31 -0
- package/src/budget.ts +59 -0
- package/src/index.ts +15 -0
- package/src/pricing.ts +49 -0
- package/src/prism-mcp.ts +352 -0
- package/src/session.ts +132 -0
- package/src/tracker.ts +56 -0
- package/src/types.ts +90 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
McpEventTracker: () => McpEventTracker,
|
|
24
|
+
PrismMCP: () => PrismMCP,
|
|
25
|
+
PrismSession: () => PrismSession,
|
|
26
|
+
PrismSessionBudgetExceededError: () => PrismSessionBudgetExceededError,
|
|
27
|
+
PrismToolCallLimitError: () => PrismToolCallLimitError,
|
|
28
|
+
SessionBudgetChecker: () => SessionBudgetChecker,
|
|
29
|
+
WrapContext: () => WrapContext,
|
|
30
|
+
lookupToolCost: () => lookupToolCost
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/tracker.ts
|
|
35
|
+
function defaultIngestUrl() {
|
|
36
|
+
const appUrl = (process.env["PRISM_APP_URL"] ?? process.env["NEXT_PUBLIC_APP_URL"] ?? "https://useprism.dev").replace(/\/$/, "");
|
|
37
|
+
return `${appUrl}/api/mcp/ingest`;
|
|
38
|
+
}
|
|
39
|
+
function orgFromKey(key) {
|
|
40
|
+
const parts = key.split("_");
|
|
41
|
+
return parts.length >= 4 ? parts[2] ?? "" : "";
|
|
42
|
+
}
|
|
43
|
+
var McpEventTracker = class {
|
|
44
|
+
constructor(key, serverName, ingestUrl) {
|
|
45
|
+
this.key = key;
|
|
46
|
+
this.ingestUrl = ingestUrl ?? defaultIngestUrl();
|
|
47
|
+
this.serverName = serverName;
|
|
48
|
+
this.orgId = orgFromKey(key);
|
|
49
|
+
}
|
|
50
|
+
async capture(event) {
|
|
51
|
+
try {
|
|
52
|
+
const full = {
|
|
53
|
+
...event,
|
|
54
|
+
event_id: crypto.randomUUID(),
|
|
55
|
+
org_id: this.orgId,
|
|
56
|
+
mcp_server_name: this.serverName
|
|
57
|
+
};
|
|
58
|
+
const res = await fetch(this.ingestUrl, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${this.key}`,
|
|
62
|
+
"Content-Type": "application/json"
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ events: [full] })
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok && res.status !== 422) {
|
|
67
|
+
console.warn(`[prism-mcp] Ingest returned ${res.status}`);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/types.ts
|
|
75
|
+
var PrismSessionBudgetExceededError = class extends Error {
|
|
76
|
+
constructor(sessionId, budgetUsd) {
|
|
77
|
+
super(
|
|
78
|
+
`[prism-mcp] Session budget of $${budgetUsd} exceeded for session ${sessionId}. Tool call blocked to prevent runaway agent costs.`
|
|
79
|
+
);
|
|
80
|
+
this.name = "PrismSessionBudgetExceededError";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var PrismToolCallLimitError = class extends Error {
|
|
84
|
+
constructor(sessionId, limit) {
|
|
85
|
+
super(
|
|
86
|
+
`[prism-mcp] Tool call limit of ${limit} reached for session ${sessionId}. Possible agent loop detected \u2014 tool call blocked.`
|
|
87
|
+
);
|
|
88
|
+
this.name = "PrismToolCallLimitError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/budget.ts
|
|
93
|
+
async function redisGet(url, token, key) {
|
|
94
|
+
const res = await fetch(`${url}/get/${encodeURIComponent(key)}`, {
|
|
95
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok) return 0;
|
|
98
|
+
const json = await res.json();
|
|
99
|
+
return parseFloat(json.result ?? "0") || 0;
|
|
100
|
+
}
|
|
101
|
+
var SessionBudgetChecker = class {
|
|
102
|
+
constructor(orgId) {
|
|
103
|
+
this.url = process.env["UPSTASH_REDIS_REST_URL"] ?? null;
|
|
104
|
+
this.token = process.env["UPSTASH_REDIS_REST_TOKEN"] ?? null;
|
|
105
|
+
this.orgId = orgId;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Throws PrismSessionBudgetExceededError or PrismToolCallLimitError
|
|
109
|
+
* if the session has exceeded its configured limits.
|
|
110
|
+
*/
|
|
111
|
+
async checkOrThrow(sessionId, sessionBudgetUsd, maxToolCallsPerSession) {
|
|
112
|
+
if (!this.url || !this.token) return;
|
|
113
|
+
if (sessionBudgetUsd != null && sessionBudgetUsd > 0) {
|
|
114
|
+
const costKey = `session:${this.orgId}:${sessionId}:cost`;
|
|
115
|
+
const current = await redisGet(this.url, this.token, costKey);
|
|
116
|
+
if (current >= sessionBudgetUsd) {
|
|
117
|
+
throw new PrismSessionBudgetExceededError(sessionId, sessionBudgetUsd);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (maxToolCallsPerSession != null && maxToolCallsPerSession > 0) {
|
|
121
|
+
const toolKey = `session:${this.orgId}:${sessionId}:tool_calls`;
|
|
122
|
+
const count = await redisGet(this.url, this.token, toolKey);
|
|
123
|
+
if (count >= maxToolCallsPerSession) {
|
|
124
|
+
throw new PrismToolCallLimitError(sessionId, maxToolCallsPerSession);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/pricing.ts
|
|
131
|
+
var BUILT_IN = {
|
|
132
|
+
// Vector DBs
|
|
133
|
+
pinecone_query: 1e-6,
|
|
134
|
+
pinecone_upsert: 2e-6,
|
|
135
|
+
weaviate_query: 8e-7,
|
|
136
|
+
qdrant_search: 5e-7,
|
|
137
|
+
// AWS
|
|
138
|
+
lambda_invoke: 2e-7,
|
|
139
|
+
s3_get_object: 4e-7,
|
|
140
|
+
s3_put_object: 5e-6,
|
|
141
|
+
dynamodb_get_item: 25e-8,
|
|
142
|
+
dynamodb_put_item: 125e-8,
|
|
143
|
+
// Search / web
|
|
144
|
+
brave_search: 3e-6,
|
|
145
|
+
serper_search: 1e-6,
|
|
146
|
+
tavily_search: 2e-6,
|
|
147
|
+
exa_search: 5e-6,
|
|
148
|
+
// Code execution
|
|
149
|
+
e2b_run_code: 14e-6,
|
|
150
|
+
code_interpreter: 14e-6,
|
|
151
|
+
// Communication
|
|
152
|
+
send_email: 1e-6
|
|
153
|
+
};
|
|
154
|
+
function lookupToolCost(toolName, overrides = {}) {
|
|
155
|
+
const all = { ...BUILT_IN, ...overrides };
|
|
156
|
+
if (toolName in all) return all[toolName];
|
|
157
|
+
const prefix = Object.keys(all).filter((k) => k.endsWith("*") && toolName.startsWith(k.slice(0, -1))).sort((a, b) => b.length - a.length)[0];
|
|
158
|
+
return prefix ? all[prefix] ?? 0 : 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/prism-mcp.ts
|
|
162
|
+
function orgFromKey2(key) {
|
|
163
|
+
const parts = key.split("_");
|
|
164
|
+
return parts.length >= 4 ? parts[2] ?? "" : "";
|
|
165
|
+
}
|
|
166
|
+
var DEFAULT_REDACT_KEYS = ["password", "token", "key", "secret", "api_key", "authorization"];
|
|
167
|
+
function redactObject(obj, redactKeys) {
|
|
168
|
+
if (typeof obj !== "object" || obj === null) return obj;
|
|
169
|
+
if (Array.isArray(obj)) return obj.map((v) => redactObject(v, redactKeys));
|
|
170
|
+
const out = {};
|
|
171
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
172
|
+
out[k] = redactKeys.some((r) => k.toLowerCase().includes(r.toLowerCase())) ? "[REDACTED]" : redactObject(v, redactKeys);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
function truncate(s, max) {
|
|
177
|
+
return s.length <= max ? s : s.slice(0, max) + "\u2026";
|
|
178
|
+
}
|
|
179
|
+
function safeJson(val, redactKeys, maxLen) {
|
|
180
|
+
try {
|
|
181
|
+
return truncate(JSON.stringify(redactObject(val, redactKeys)), maxLen);
|
|
182
|
+
} catch {
|
|
183
|
+
return "[unserializable]";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isAsyncIterable(val) {
|
|
187
|
+
return val != null && typeof val[Symbol.asyncIterator] === "function";
|
|
188
|
+
}
|
|
189
|
+
async function* proxyAsyncIterable(source, start, onEnd) {
|
|
190
|
+
let threw = false;
|
|
191
|
+
try {
|
|
192
|
+
for await (const value of source) {
|
|
193
|
+
yield value;
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
threw = true;
|
|
197
|
+
throw err;
|
|
198
|
+
} finally {
|
|
199
|
+
onEnd(Date.now() - start, threw);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
var WrapContext = class {
|
|
203
|
+
constructor() {
|
|
204
|
+
/** @internal - read by _wrap() after fn() completes */
|
|
205
|
+
this._actualCostUsd = null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Override the catalog-estimated tool cost with the real billing figure.
|
|
209
|
+
* Call this inside your tool handler when you have access to actual cost data
|
|
210
|
+
* (e.g. from AWS SDK response metadata, a billing header, or a usage API).
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* await prismMcp.wrapToolCall("invoke_lambda", async (ctx) => {
|
|
214
|
+
* const res = await lambda.invoke({ FunctionName: "fn", Payload: payload });
|
|
215
|
+
* const billedMs = res.$metadata.httpHeaders?.["x-amz-billed-duration-ms"] ?? "0";
|
|
216
|
+
* ctx.reportActualCost(parseInt(billedMs) * 0.000016667 / 1000);
|
|
217
|
+
* return res;
|
|
218
|
+
* });
|
|
219
|
+
*/
|
|
220
|
+
reportActualCost(usd) {
|
|
221
|
+
this._actualCostUsd = usd;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var PrismMCP = class {
|
|
225
|
+
constructor(options = {}) {
|
|
226
|
+
const key = options.prismKey ?? process.env["PRISM_API_KEY"] ?? "";
|
|
227
|
+
if (!key) {
|
|
228
|
+
console.warn("[prism-mcp] PRISM_API_KEY not set \u2014 MCP observability disabled.");
|
|
229
|
+
}
|
|
230
|
+
this.opts = {
|
|
231
|
+
project: options.project ?? process.env["PRISM_PROJECT"] ?? "",
|
|
232
|
+
team: options.team ?? process.env["PRISM_TEAM"] ?? "",
|
|
233
|
+
environment: options.environment ?? process.env["PRISM_ENVIRONMENT"] ?? "production",
|
|
234
|
+
sessionId: options.sessionId ?? crypto.randomUUID(),
|
|
235
|
+
serverName: options.serverName ?? "mcp-server",
|
|
236
|
+
sessionBudgetUsd: options.sessionBudgetUsd,
|
|
237
|
+
maxToolCallsPerSession: options.maxToolCallsPerSession,
|
|
238
|
+
captureInputs: options.captureInputs ?? false,
|
|
239
|
+
captureOutputs: options.captureOutputs ?? false
|
|
240
|
+
};
|
|
241
|
+
this.redactKeys = options.redactKeys ?? DEFAULT_REDACT_KEYS;
|
|
242
|
+
this.tracker = new McpEventTracker(key, this.opts.serverName, options.ingestUrl);
|
|
243
|
+
this.budget = new SessionBudgetChecker(orgFromKey2(key));
|
|
244
|
+
}
|
|
245
|
+
// ── Core primitive wrapper ─────────────────────────────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Internal wrapper used by all four MCP primitives.
|
|
248
|
+
* Handles: budget check, I/O capture, streaming proxy, fire-and-forget telemetry,
|
|
249
|
+
* and actual cost self-reporting via ctx.reportActualCost().
|
|
250
|
+
*/
|
|
251
|
+
async _wrap(primitiveType, name, fn, extra = {}) {
|
|
252
|
+
await this.budget.checkOrThrow(
|
|
253
|
+
this.opts.sessionId,
|
|
254
|
+
this.opts.sessionBudgetUsd,
|
|
255
|
+
this.opts.maxToolCallsPerSession
|
|
256
|
+
);
|
|
257
|
+
const start = Date.now();
|
|
258
|
+
let status = "ok";
|
|
259
|
+
let errorMsg = "";
|
|
260
|
+
let result;
|
|
261
|
+
const ctx = new WrapContext();
|
|
262
|
+
const eventTags = { ...extra.tags };
|
|
263
|
+
if (this.opts.captureInputs && extra.inputs != null) {
|
|
264
|
+
eventTags["tool_input"] = safeJson(extra.inputs, this.redactKeys, 1e3);
|
|
265
|
+
}
|
|
266
|
+
const estimatedCost = primitiveType === "tool" ? lookupToolCost(name) : 0;
|
|
267
|
+
const fireEvent = (latencyMs, finalStatus) => {
|
|
268
|
+
const actualCostUsd = ctx._actualCostUsd;
|
|
269
|
+
this.tracker.capture({
|
|
270
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 23),
|
|
271
|
+
session_id: this.opts.sessionId,
|
|
272
|
+
project_id: this.opts.project,
|
|
273
|
+
team_id: this.opts.team,
|
|
274
|
+
user_id: "",
|
|
275
|
+
environment: this.opts.environment,
|
|
276
|
+
tool_name: name,
|
|
277
|
+
downstream_resource: extra.downstreamResource ?? "",
|
|
278
|
+
execution_latency_ms: latencyMs,
|
|
279
|
+
tool_cost_usd: actualCostUsd ?? estimatedCost,
|
|
280
|
+
cost_status: actualCostUsd != null ? "actual" : "estimated",
|
|
281
|
+
status: finalStatus,
|
|
282
|
+
error_message: errorMsg,
|
|
283
|
+
llm_request_id: extra.llmRequestId ?? "",
|
|
284
|
+
primitive_type: primitiveType,
|
|
285
|
+
tags: eventTags
|
|
286
|
+
}).catch(() => {
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
result = await fn(ctx);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
status = "error";
|
|
293
|
+
errorMsg = err instanceof Error ? err.message : String(err);
|
|
294
|
+
fireEvent(Date.now() - start, status);
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
if (this.opts.captureOutputs && result != null && !isAsyncIterable(result)) {
|
|
298
|
+
eventTags["tool_output"] = safeJson(result, this.redactKeys, 1e3);
|
|
299
|
+
}
|
|
300
|
+
if (isAsyncIterable(result)) {
|
|
301
|
+
return proxyAsyncIterable(
|
|
302
|
+
result,
|
|
303
|
+
start,
|
|
304
|
+
(latencyMs, threw) => {
|
|
305
|
+
fireEvent(latencyMs, threw ? "error" : "ok");
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
fireEvent(Date.now() - start, status);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
// ── Public API: tools ──────────────────────────────────────────────────────
|
|
313
|
+
/**
|
|
314
|
+
* Wrap a tools/call execution with Prism instrumentation.
|
|
315
|
+
*/
|
|
316
|
+
async wrapToolCall(toolName, fn, extra = {}) {
|
|
317
|
+
return this._wrap("tool", toolName, fn, extra);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Drop-in patch for MCP SDK's CallToolRequestSchema handler.
|
|
321
|
+
* Automatically passes req.params.arguments as inputs when captureInputs: true.
|
|
322
|
+
* ctx is available for reportActualCost() inside the handler.
|
|
323
|
+
*/
|
|
324
|
+
patchHandler(handler) {
|
|
325
|
+
const self = this;
|
|
326
|
+
return async function patchedHandler(req) {
|
|
327
|
+
return self.wrapToolCall(
|
|
328
|
+
req.params.name,
|
|
329
|
+
(ctx) => handler(req, ctx),
|
|
330
|
+
{ inputs: req.params.arguments }
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// ── Public API: resources ──────────────────────────────────────────────────
|
|
335
|
+
/**
|
|
336
|
+
* Wrap a resources/read execution with Prism instrumentation.
|
|
337
|
+
*
|
|
338
|
+
* @param resourceUri - The resource URI being read (e.g. "file:///path/to/file")
|
|
339
|
+
*/
|
|
340
|
+
async wrapResourceRead(resourceUri, fn, extra = {}) {
|
|
341
|
+
return this._wrap("resource", resourceUri, fn, extra);
|
|
342
|
+
}
|
|
343
|
+
patchResourceHandler(handler) {
|
|
344
|
+
const self = this;
|
|
345
|
+
return async function patchedResourceHandler(req) {
|
|
346
|
+
return self.wrapResourceRead(req.params.uri, (ctx) => handler(req, ctx));
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// ── Public API: prompts ────────────────────────────────────────────────────
|
|
350
|
+
async wrapPromptGet(promptName, fn, extra = {}) {
|
|
351
|
+
return this._wrap("prompt", promptName, fn, extra);
|
|
352
|
+
}
|
|
353
|
+
patchPromptHandler(handler) {
|
|
354
|
+
const self = this;
|
|
355
|
+
return async function patchedPromptHandler(req) {
|
|
356
|
+
return self.wrapPromptGet(req.params.name, (ctx) => handler(req, ctx));
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
// ── Public API: sampling ───────────────────────────────────────────────────
|
|
360
|
+
/**
|
|
361
|
+
* Wrap the MCP client's sampling/createMessage callback so LLM calls
|
|
362
|
+
* requested by the MCP server appear in the session timeline.
|
|
363
|
+
*
|
|
364
|
+
* Usage with @modelcontextprotocol/sdk:
|
|
365
|
+
* client.setRequestHandler(
|
|
366
|
+
* CreateMessageRequestSchema,
|
|
367
|
+
* prismMcp.wrapSamplingHandler(async (req) => {
|
|
368
|
+
* const res = await openai.chat.completions.create({ ... });
|
|
369
|
+
* return { role: "assistant", content: [{ type: "text", text: res.choices[0].message.content }] };
|
|
370
|
+
* })
|
|
371
|
+
* );
|
|
372
|
+
*/
|
|
373
|
+
wrapSamplingHandler(handler) {
|
|
374
|
+
const self = this;
|
|
375
|
+
return async function patchedSamplingHandler(req) {
|
|
376
|
+
const modelHint = req.params?.modelPreferences?.hints?.[0]?.name ?? "sampling";
|
|
377
|
+
return self._wrap("sampling", modelHint, (_ctx) => handler(req));
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/** The session_id assigned to this instance (useful for logging). */
|
|
381
|
+
get sessionId() {
|
|
382
|
+
return this.opts.sessionId;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/session.ts
|
|
387
|
+
var PrismSession = class {
|
|
388
|
+
constructor(options = {}) {
|
|
389
|
+
this.key = options.prismKey ?? process.env["PRISM_API_KEY"] ?? "";
|
|
390
|
+
this.project = options.project ?? process.env["PRISM_PROJECT"] ?? "";
|
|
391
|
+
this.team = options.team ?? process.env["PRISM_TEAM"] ?? "";
|
|
392
|
+
this.environment = options.environment ?? process.env["PRISM_ENVIRONMENT"] ?? "production";
|
|
393
|
+
this.sessionId = options.sessionId ?? crypto.randomUUID();
|
|
394
|
+
this.sharedOpts = options;
|
|
395
|
+
if (!this.key) {
|
|
396
|
+
console.warn("[prism-mcp] PrismSession: PRISM_API_KEY not set \u2014 observability disabled.");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Create a PrismMCP instance bound to this session.
|
|
401
|
+
* All servers created from the same session share the same session_id.
|
|
402
|
+
*/
|
|
403
|
+
createServer(perServer = {}) {
|
|
404
|
+
return new PrismMCP({
|
|
405
|
+
prismKey: this.key,
|
|
406
|
+
project: this.project,
|
|
407
|
+
team: this.team,
|
|
408
|
+
environment: this.environment,
|
|
409
|
+
sessionId: this.sessionId,
|
|
410
|
+
sessionBudgetUsd: this.sharedOpts.sessionBudgetUsd,
|
|
411
|
+
maxToolCallsPerSession: this.sharedOpts.maxToolCallsPerSession,
|
|
412
|
+
captureInputs: this.sharedOpts.captureInputs,
|
|
413
|
+
captureOutputs: this.sharedOpts.captureOutputs,
|
|
414
|
+
redactKeys: this.sharedOpts.redactKeys,
|
|
415
|
+
ingestUrl: this.sharedOpts.ingestUrl,
|
|
416
|
+
// per-server overrides
|
|
417
|
+
serverName: perServer.serverName
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Returns options to pass to the Prism LLM SDK clients (@prism-llm-labs/sdk)
|
|
422
|
+
* so that LLM completions share this session's session_id.
|
|
423
|
+
*
|
|
424
|
+
* import { OpenAI } from "@prism-llm-labs/sdk";
|
|
425
|
+
* const openai = new OpenAI(session.toLLMOptions());
|
|
426
|
+
*/
|
|
427
|
+
toLLMOptions() {
|
|
428
|
+
return {
|
|
429
|
+
prismKey: this.key,
|
|
430
|
+
project: this.project,
|
|
431
|
+
team: this.team,
|
|
432
|
+
environment: this.environment,
|
|
433
|
+
sessionId: this.sessionId
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
438
|
+
0 && (module.exports = {
|
|
439
|
+
McpEventTracker,
|
|
440
|
+
PrismMCP,
|
|
441
|
+
PrismSession,
|
|
442
|
+
PrismSessionBudgetExceededError,
|
|
443
|
+
PrismToolCallLimitError,
|
|
444
|
+
SessionBudgetChecker,
|
|
445
|
+
WrapContext,
|
|
446
|
+
lookupToolCost
|
|
447
|
+
});
|
|
448
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/tracker.ts","../src/types.ts","../src/budget.ts","../src/pricing.ts","../src/prism-mcp.ts","../src/session.ts"],"sourcesContent":["export { PrismMCP, WrapContext } from \"./prism-mcp\";\nexport { PrismSession } from \"./session\";\nexport type { PrismSessionOptions, PerServerOptions } from \"./session\";\nexport { McpEventTracker } from \"./tracker\";\nexport { SessionBudgetChecker } from \"./budget\";\nexport { lookupToolCost } from \"./pricing\";\nexport type {\n McpEvent,\n McpPrimitiveType,\n PrismMcpOptions,\n} from \"./types\";\nexport {\n PrismSessionBudgetExceededError,\n PrismToolCallLimitError,\n} from \"./types\";\n","import type { McpEvent } from \"./types\";\n\nfunction defaultIngestUrl(): string {\n const appUrl = (\n process.env[\"PRISM_APP_URL\"] ??\n process.env[\"NEXT_PUBLIC_APP_URL\"] ??\n \"https://useprism.dev\"\n ).replace(/\\/$/, \"\");\n return `${appUrl}/api/mcp/ingest`;\n}\n\nfunction orgFromKey(key: string): string {\n const parts = key.split(\"_\");\n return parts.length >= 4 ? (parts[2] ?? \"\") : \"\";\n}\n\nexport class McpEventTracker {\n private readonly key: string;\n private readonly ingestUrl: string;\n private readonly serverName: string;\n private readonly orgId: string;\n\n constructor(key: string, serverName: string, ingestUrl?: string) {\n this.key = key;\n this.ingestUrl = ingestUrl ?? defaultIngestUrl();\n this.serverName = serverName;\n this.orgId = orgFromKey(key);\n }\n\n async capture(event: Omit<McpEvent, \"event_id\" | \"org_id\" | \"mcp_server_name\">): Promise<void> {\n try {\n const full: McpEvent = {\n ...event,\n event_id: crypto.randomUUID(),\n org_id: this.orgId,\n mcp_server_name: this.serverName,\n };\n\n const res = await fetch(this.ingestUrl, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.key}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ events: [full] }),\n });\n\n if (!res.ok && res.status !== 422) {\n // Silently ignore — observability must never break the agent\n console.warn(`[prism-mcp] Ingest returned ${res.status}`);\n }\n } catch {\n // Never propagate\n }\n }\n}\n","export type McpPrimitiveType = \"tool\" | \"resource\" | \"prompt\" | \"sampling\";\n\nexport interface McpEvent {\n event_id: string;\n timestamp: string;\n session_id: string;\n org_id: string;\n project_id: string;\n team_id: string;\n user_id: string;\n environment: string;\n mcp_server_name: string;\n /** tool name, resource URI, prompt name, or model hint for sampling */\n tool_name: string;\n downstream_resource: string;\n execution_latency_ms: number;\n tool_cost_usd: number;\n status: \"ok\" | \"error\" | \"timeout\";\n error_message: string;\n llm_request_id: string;\n primitive_type: McpPrimitiveType;\n /**\n * Whether tool_cost_usd is an estimate from the built-in catalog (\"estimated\")\n * or a real figure provided by the tool via ctx.reportActualCost() (\"actual\").\n */\n cost_status: \"estimated\" | \"actual\";\n tags: Record<string, string>;\n}\n\nexport interface PrismMcpOptions {\n /** Prism API key — or set PRISM_API_KEY env var */\n prismKey?: string;\n /** Project ID for cost attribution */\n project?: string;\n /** Team attribution tag */\n team?: string;\n /** \"production\" | \"staging\" | \"development\" */\n environment?: string;\n /** Explicit session ID. Auto-generated UUID if omitted. */\n sessionId?: string;\n /** MCP server name shown in the dashboard */\n serverName?: string;\n /** Override ingest URL (for testing) */\n ingestUrl?: string;\n /**\n * Session budget in USD. Tool/resource/prompt calls are blocked when the\n * combined session cost exceeds this value.\n */\n sessionBudgetUsd?: number;\n /**\n * Maximum MCP primitive calls per session. Blocks further calls when exceeded.\n * Loop detection guard — default unlimited.\n */\n maxToolCallsPerSession?: number;\n /**\n * Log call arguments into tags['tool_input'] (truncated to 1000 chars).\n * Opt-in only — disabled by default for privacy.\n */\n captureInputs?: boolean;\n /**\n * Log call results into tags['tool_output'] (truncated to 1000 chars).\n * Opt-in only — disabled by default for privacy.\n */\n captureOutputs?: boolean;\n /**\n * Keys to redact from captured inputs/outputs.\n * Default: ['password', 'token', 'key', 'secret', 'api_key', 'authorization']\n */\n redactKeys?: string[];\n}\n\nexport class PrismSessionBudgetExceededError extends Error {\n constructor(sessionId: string, budgetUsd: number) {\n super(\n `[prism-mcp] Session budget of $${budgetUsd} exceeded for session ${sessionId}. ` +\n `Tool call blocked to prevent runaway agent costs.`,\n );\n this.name = \"PrismSessionBudgetExceededError\";\n }\n}\n\nexport class PrismToolCallLimitError extends Error {\n constructor(sessionId: string, limit: number) {\n super(\n `[prism-mcp] Tool call limit of ${limit} reached for session ${sessionId}. ` +\n `Possible agent loop detected — tool call blocked.`,\n );\n this.name = \"PrismToolCallLimitError\";\n }\n}\n","/**\n * Session budget circuit breaker.\n * Checks Redis session cost + tool-call counters before each tool invocation.\n * If the UPSTASH env vars are absent, checks are skipped (graceful degradation).\n */\n\nimport {\n PrismSessionBudgetExceededError,\n PrismToolCallLimitError,\n} from \"./types\";\n\nasync function redisGet(url: string, token: string, key: string): Promise<number> {\n const res = await fetch(`${url}/get/${encodeURIComponent(key)}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) return 0;\n const json = await res.json() as { result: string | null };\n return parseFloat(json.result ?? \"0\") || 0;\n}\n\nexport class SessionBudgetChecker {\n private readonly url: string | null;\n private readonly token: string | null;\n private readonly orgId: string;\n\n constructor(orgId: string) {\n this.url = process.env[\"UPSTASH_REDIS_REST_URL\"] ?? null;\n this.token = process.env[\"UPSTASH_REDIS_REST_TOKEN\"] ?? null;\n this.orgId = orgId;\n }\n\n /**\n * Throws PrismSessionBudgetExceededError or PrismToolCallLimitError\n * if the session has exceeded its configured limits.\n */\n async checkOrThrow(\n sessionId: string,\n sessionBudgetUsd?: number,\n maxToolCallsPerSession?: number,\n ): Promise<void> {\n if (!this.url || !this.token) return; // No Redis configured — skip\n\n if (sessionBudgetUsd != null && sessionBudgetUsd > 0) {\n const costKey = `session:${this.orgId}:${sessionId}:cost`;\n const current = await redisGet(this.url, this.token, costKey);\n if (current >= sessionBudgetUsd) {\n throw new PrismSessionBudgetExceededError(sessionId, sessionBudgetUsd);\n }\n }\n\n if (maxToolCallsPerSession != null && maxToolCallsPerSession > 0) {\n const toolKey = `session:${this.orgId}:${sessionId}:tool_calls`;\n const count = await redisGet(this.url, this.token, toolKey);\n if (count >= maxToolCallsPerSession) {\n throw new PrismToolCallLimitError(sessionId, maxToolCallsPerSession);\n }\n }\n }\n}\n","/**\n * Built-in tool cost estimates (USD per call).\n * Mirrors apps/web/lib/pricing/tool-catalog.ts — keep in sync.\n */\n\nconst BUILT_IN: Record<string, number> = {\n // Vector DBs\n pinecone_query: 0.000001,\n pinecone_upsert: 0.000002,\n weaviate_query: 0.0000008,\n qdrant_search: 0.0000005,\n // AWS\n lambda_invoke: 0.0000002,\n s3_get_object: 0.0000004,\n s3_put_object: 0.000005,\n dynamodb_get_item: 0.00000025,\n dynamodb_put_item: 0.00000125,\n // Search / web\n brave_search: 0.000003,\n serper_search: 0.000001,\n tavily_search: 0.000002,\n exa_search: 0.000005,\n // Code execution\n e2b_run_code: 0.000014,\n code_interpreter: 0.000014,\n // Communication\n send_email: 0.000001,\n};\n\n/**\n * Look up estimated cost for a tool name.\n * Exact match, then prefix match (\"pinecone_*\"), then 0.\n */\nexport function lookupToolCost(\n toolName: string,\n overrides: Record<string, number> = {},\n): number {\n const all = { ...BUILT_IN, ...overrides };\n\n // Exact\n if (toolName in all) return all[toolName]!;\n\n // Prefix: find the longest matching prefix key ending in \"_*\" or \":*\"\n const prefix = Object.keys(all)\n .filter((k) => k.endsWith(\"*\") && toolName.startsWith(k.slice(0, -1)))\n .sort((a, b) => b.length - a.length)[0];\n\n return prefix ? (all[prefix] ?? 0) : 0;\n}\n","/**\n * PrismMCP — instruments any MCP Server with full observability:\n * - tools/call → wrapToolCall() / patchHandler()\n * - resources/read → wrapResourceRead() / patchResourceHandler()\n * - prompts/get → wrapPromptGet() / patchPromptHandler()\n * - sampling/createMessage → wrapSamplingHandler()\n *\n * Features:\n * - Session budget circuit breaker (throws before execution if over budget)\n * - Tool loop detection (throws if max_tool_calls_per_session exceeded)\n * - Opt-in I/O capture (captureInputs / captureOutputs)\n * - Streaming tool latency: correctly measures time-to-stream-end, not first-chunk\n */\n\nimport type { McpPrimitiveType, PrismMcpOptions } from \"./types\";\nimport { McpEventTracker } from \"./tracker\";\nimport { SessionBudgetChecker } from \"./budget\";\nimport { lookupToolCost } from \"./pricing\";\n\n// ── Private helpers ────────────────────────────────────────────────────────────\n\nfunction orgFromKey(key: string): string {\n const parts = key.split(\"_\");\n return parts.length >= 4 ? (parts[2] ?? \"\") : \"\";\n}\n\nconst DEFAULT_REDACT_KEYS = [\"password\", \"token\", \"key\", \"secret\", \"api_key\", \"authorization\"];\n\nfunction redactObject(\n obj: unknown,\n redactKeys: string[],\n): unknown {\n if (typeof obj !== \"object\" || obj === null) return obj;\n if (Array.isArray(obj)) return obj.map((v) => redactObject(v, redactKeys));\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {\n out[k] = redactKeys.some((r) => k.toLowerCase().includes(r.toLowerCase()))\n ? \"[REDACTED]\"\n : redactObject(v, redactKeys);\n }\n return out;\n}\n\nfunction truncate(s: string, max: number): string {\n return s.length <= max ? s : s.slice(0, max) + \"…\";\n}\n\nfunction safeJson(val: unknown, redactKeys: string[], maxLen: number): string {\n try {\n return truncate(JSON.stringify(redactObject(val, redactKeys)), maxLen);\n } catch {\n return \"[unserializable]\";\n }\n}\n\n// ── Streaming proxy (Epic 6) ───────────────────────────────────────────────────\n\nfunction isAsyncIterable(val: unknown): val is AsyncIterable<unknown> {\n return val != null &&\n typeof (val as AsyncIterable<unknown>)[Symbol.asyncIterator] === \"function\";\n}\n\n/**\n * Wraps an async iterable so that `onEnd` is called with the total elapsed ms\n * when the stream is fully consumed or throws. All values are forwarded unchanged.\n */\nasync function* proxyAsyncIterable<T>(\n source: AsyncIterable<T>,\n start: number,\n onEnd: (latencyMs: number, threw: boolean) => void,\n): AsyncGenerator<T> {\n let threw = false;\n try {\n for await (const value of source) {\n yield value;\n }\n } catch (err) {\n threw = true;\n throw err;\n } finally {\n onEnd(Date.now() - start, threw);\n }\n}\n\n// ── WrapContext — passed to the fn() callback for actual cost self-reporting ──\n\nexport class WrapContext {\n /** @internal - read by _wrap() after fn() completes */\n _actualCostUsd: number | null = null;\n\n /**\n * Override the catalog-estimated tool cost with the real billing figure.\n * Call this inside your tool handler when you have access to actual cost data\n * (e.g. from AWS SDK response metadata, a billing header, or a usage API).\n *\n * @example\n * await prismMcp.wrapToolCall(\"invoke_lambda\", async (ctx) => {\n * const res = await lambda.invoke({ FunctionName: \"fn\", Payload: payload });\n * const billedMs = res.$metadata.httpHeaders?.[\"x-amz-billed-duration-ms\"] ?? \"0\";\n * ctx.reportActualCost(parseInt(billedMs) * 0.000016667 / 1000);\n * return res;\n * });\n */\n reportActualCost(usd: number): void {\n this._actualCostUsd = usd;\n }\n}\n\n// ── Main class ────────────────────────────────────────────────────────────────\n\nexport class PrismMCP {\n private readonly tracker: McpEventTracker;\n private readonly budget: SessionBudgetChecker;\n private readonly redactKeys: string[];\n private readonly opts: Required<Pick<PrismMcpOptions,\n \"project\" | \"team\" | \"environment\" | \"sessionId\" | \"serverName\" |\n \"captureInputs\" | \"captureOutputs\">> &\n Pick<PrismMcpOptions, \"sessionBudgetUsd\" | \"maxToolCallsPerSession\">;\n\n constructor(options: PrismMcpOptions = {}) {\n const key = options.prismKey ?? process.env[\"PRISM_API_KEY\"] ?? \"\";\n if (!key) {\n console.warn(\"[prism-mcp] PRISM_API_KEY not set — MCP observability disabled.\");\n }\n\n this.opts = {\n project: options.project ?? process.env[\"PRISM_PROJECT\"] ?? \"\",\n team: options.team ?? process.env[\"PRISM_TEAM\"] ?? \"\",\n environment: options.environment ?? process.env[\"PRISM_ENVIRONMENT\"] ?? \"production\",\n sessionId: options.sessionId ?? crypto.randomUUID(),\n serverName: options.serverName ?? \"mcp-server\",\n sessionBudgetUsd: options.sessionBudgetUsd,\n maxToolCallsPerSession: options.maxToolCallsPerSession,\n captureInputs: options.captureInputs ?? false,\n captureOutputs: options.captureOutputs ?? false,\n };\n this.redactKeys = options.redactKeys ?? DEFAULT_REDACT_KEYS;\n this.tracker = new McpEventTracker(key, this.opts.serverName, options.ingestUrl);\n this.budget = new SessionBudgetChecker(orgFromKey(key));\n }\n\n // ── Core primitive wrapper ─────────────────────────────────────────────────\n\n /**\n * Internal wrapper used by all four MCP primitives.\n * Handles: budget check, I/O capture, streaming proxy, fire-and-forget telemetry,\n * and actual cost self-reporting via ctx.reportActualCost().\n */\n private async _wrap<T>(\n primitiveType: McpPrimitiveType,\n name: string,\n fn: (ctx: WrapContext) => Promise<T>,\n extra: {\n llmRequestId?: string;\n tags?: Record<string, string>;\n downstreamResource?: string;\n inputs?: Record<string, unknown>;\n } = {},\n ): Promise<T> {\n // 1. Pre-call budget/loop guard\n await this.budget.checkOrThrow(\n this.opts.sessionId,\n this.opts.sessionBudgetUsd,\n this.opts.maxToolCallsPerSession,\n );\n\n const start = Date.now();\n let status: \"ok\" | \"error\" | \"timeout\" = \"ok\";\n let errorMsg = \"\";\n let result: T;\n const ctx = new WrapContext();\n\n // Build tags — I/O capture applied here\n const eventTags: Record<string, string> = { ...extra.tags };\n if (this.opts.captureInputs && extra.inputs != null) {\n eventTags[\"tool_input\"] = safeJson(extra.inputs, this.redactKeys, 1000);\n }\n\n // Cost lookup — overridden by ctx.reportActualCost() if called during fn()\n const estimatedCost = primitiveType === \"tool\" ? lookupToolCost(name) : 0;\n\n const fireEvent = (latencyMs: number, finalStatus: \"ok\" | \"error\" | \"timeout\") => {\n const actualCostUsd = ctx._actualCostUsd;\n this.tracker.capture({\n timestamp: new Date().toISOString().replace(\"T\", \" \").slice(0, 23),\n session_id: this.opts.sessionId,\n project_id: this.opts.project,\n team_id: this.opts.team,\n user_id: \"\",\n environment: this.opts.environment,\n tool_name: name,\n downstream_resource: extra.downstreamResource ?? \"\",\n execution_latency_ms: latencyMs,\n tool_cost_usd: actualCostUsd ?? estimatedCost,\n cost_status: actualCostUsd != null ? \"actual\" : \"estimated\",\n status: finalStatus,\n error_message: errorMsg,\n llm_request_id: extra.llmRequestId ?? \"\",\n primitive_type: primitiveType,\n tags: eventTags,\n }).catch(() => {});\n };\n\n try {\n result = await fn(ctx);\n } catch (err) {\n status = \"error\";\n errorMsg = err instanceof Error ? err.message : String(err);\n fireEvent(Date.now() - start, status);\n throw err;\n }\n\n // Capture output if requested (non-streaming path)\n if (this.opts.captureOutputs && result != null && !isAsyncIterable(result)) {\n eventTags[\"tool_output\"] = safeJson(result, this.redactKeys, 1000);\n }\n\n // Streaming path — defer telemetry until stream is exhausted\n if (isAsyncIterable(result)) {\n return proxyAsyncIterable(\n result as AsyncIterable<unknown>,\n start,\n (latencyMs, threw) => {\n fireEvent(latencyMs, threw ? \"error\" : \"ok\");\n },\n ) as unknown as T;\n }\n\n // Non-streaming path — fire immediately\n fireEvent(Date.now() - start, status);\n return result;\n }\n\n // ── Public API: tools ──────────────────────────────────────────────────────\n\n /**\n * Wrap a tools/call execution with Prism instrumentation.\n */\n async wrapToolCall<T>(\n toolName: string,\n fn: (ctx: WrapContext) => Promise<T>,\n extra: {\n llmRequestId?: string;\n tags?: Record<string, string>;\n downstreamResource?: string;\n /** Raw tool arguments — captured if captureInputs: true */\n inputs?: Record<string, unknown>;\n } = {},\n ): Promise<T> {\n return this._wrap(\"tool\", toolName, fn, extra);\n }\n\n /**\n * Drop-in patch for MCP SDK's CallToolRequestSchema handler.\n * Automatically passes req.params.arguments as inputs when captureInputs: true.\n * ctx is available for reportActualCost() inside the handler.\n */\n patchHandler<TReq extends { params: { name: string; arguments?: Record<string, unknown> } }, TRes>(\n handler: (req: TReq, ctx: WrapContext) => Promise<TRes>,\n ): (req: TReq) => Promise<TRes> {\n const self = this;\n return async function patchedHandler(req: TReq): Promise<TRes> {\n return self.wrapToolCall(\n req.params.name,\n (ctx) => handler(req, ctx),\n { inputs: req.params.arguments },\n );\n };\n }\n\n // ── Public API: resources ──────────────────────────────────────────────────\n\n /**\n * Wrap a resources/read execution with Prism instrumentation.\n *\n * @param resourceUri - The resource URI being read (e.g. \"file:///path/to/file\")\n */\n async wrapResourceRead<T>(\n resourceUri: string,\n fn: (ctx: WrapContext) => Promise<T>,\n extra: {\n llmRequestId?: string;\n tags?: Record<string, string>;\n } = {},\n ): Promise<T> {\n return this._wrap(\"resource\", resourceUri, fn, extra);\n }\n\n patchResourceHandler<TReq extends { params: { uri: string } }, TRes>(\n handler: (req: TReq, ctx: WrapContext) => Promise<TRes>,\n ): (req: TReq) => Promise<TRes> {\n const self = this;\n return async function patchedResourceHandler(req: TReq): Promise<TRes> {\n return self.wrapResourceRead(req.params.uri, (ctx) => handler(req, ctx));\n };\n }\n\n // ── Public API: prompts ────────────────────────────────────────────────────\n\n async wrapPromptGet<T>(\n promptName: string,\n fn: (ctx: WrapContext) => Promise<T>,\n extra: {\n llmRequestId?: string;\n tags?: Record<string, string>;\n } = {},\n ): Promise<T> {\n return this._wrap(\"prompt\", promptName, fn, extra);\n }\n\n patchPromptHandler<TReq extends { params: { name: string } }, TRes>(\n handler: (req: TReq, ctx: WrapContext) => Promise<TRes>,\n ): (req: TReq) => Promise<TRes> {\n const self = this;\n return async function patchedPromptHandler(req: TReq): Promise<TRes> {\n return self.wrapPromptGet(req.params.name, (ctx) => handler(req, ctx));\n };\n }\n\n // ── Public API: sampling ───────────────────────────────────────────────────\n\n /**\n * Wrap the MCP client's sampling/createMessage callback so LLM calls\n * requested by the MCP server appear in the session timeline.\n *\n * Usage with @modelcontextprotocol/sdk:\n * client.setRequestHandler(\n * CreateMessageRequestSchema,\n * prismMcp.wrapSamplingHandler(async (req) => {\n * const res = await openai.chat.completions.create({ ... });\n * return { role: \"assistant\", content: [{ type: \"text\", text: res.choices[0].message.content }] };\n * })\n * );\n */\n wrapSamplingHandler<\n TReq extends { params?: { modelPreferences?: { hints?: Array<{ name?: string }> } } },\n TRes,\n >(\n handler: (req: TReq) => Promise<TRes>,\n ): (req: TReq) => Promise<TRes> {\n const self = this;\n return async function patchedSamplingHandler(req: TReq): Promise<TRes> {\n // Use model hint as the \"tool_name\" for display in the session timeline\n const modelHint =\n req.params?.modelPreferences?.hints?.[0]?.name ?? \"sampling\";\n return self._wrap(\"sampling\", modelHint, (_ctx) => handler(req));\n };\n }\n\n /** The session_id assigned to this instance (useful for logging). */\n get sessionId(): string { return this.opts.sessionId; }\n}\n","/**\n * PrismSession — shared session context for multi-server agent runs.\n *\n * Creates a single session_id that is automatically threaded through:\n * - Multiple PrismMCP server instances (files, database, search, etc.)\n * - LLM SDK clients (OpenAI, Anthropic) via toLLMOptions()\n *\n * This ensures every LLM call and every tool/resource/prompt call in one\n * agent run appears under the same session in /dashboard/sessions/[id].\n *\n * Usage:\n * const session = new PrismSession({\n * prismKey: process.env.PRISM_API_KEY,\n * project: \"customer-support\",\n * sessionBudgetUsd: 2.00, // hard cap for the whole agent run\n * });\n *\n * // MCP servers — all share session.sessionId automatically\n * const filesMcp = session.createServer({ serverName: \"files\" });\n * const dbMcp = session.createServer({ serverName: \"database\" });\n * const searchMcp = session.createServer({ serverName: \"search\" });\n *\n * // LLM SDK client — same session_id\n * import { OpenAI } from \"@prism-llm-labs/sdk\";\n * const openai = new OpenAI(session.toLLMOptions());\n */\n\nimport { PrismMCP } from \"./prism-mcp\";\nimport type { PrismMcpOptions } from \"./types\";\n\nexport interface PrismSessionOptions {\n /** Prism API key — or set PRISM_API_KEY env var */\n prismKey?: string;\n /** Project ID for cost attribution */\n project?: string;\n /** Team attribution tag */\n team?: string;\n /** \"production\" | \"staging\" | \"development\" */\n environment?: string;\n /**\n * Explicit session ID. Auto-generated UUID if omitted.\n * Pass an explicit ID when the session ID is determined by an external\n * orchestrator (e.g. a LangGraph run ID, a task queue job ID).\n */\n sessionId?: string;\n /** Hard cost cap across ALL servers in this session */\n sessionBudgetUsd?: number;\n /** Hard tool-call cap across ALL servers in this session */\n maxToolCallsPerSession?: number;\n /** Opt-in I/O capture for all servers created from this session */\n captureInputs?: boolean;\n captureOutputs?: boolean;\n redactKeys?: string[];\n /** Override ingest URL (for testing) */\n ingestUrl?: string;\n}\n\n/** Options for individual servers within a session */\nexport type PerServerOptions = Omit<\n PrismMcpOptions,\n \"prismKey\" | \"project\" | \"team\" | \"environment\" | \"sessionId\" |\n \"sessionBudgetUsd\" | \"maxToolCallsPerSession\" | \"captureInputs\" |\n \"captureOutputs\" | \"redactKeys\" | \"ingestUrl\"\n>;\n\nexport class PrismSession {\n readonly sessionId: string;\n\n private readonly key: string;\n private readonly project: string;\n private readonly team: string;\n private readonly environment: string;\n private readonly sharedOpts: PrismSessionOptions;\n\n constructor(options: PrismSessionOptions = {}) {\n this.key = options.prismKey ?? process.env[\"PRISM_API_KEY\"] ?? \"\";\n this.project = options.project ?? process.env[\"PRISM_PROJECT\"] ?? \"\";\n this.team = options.team ?? process.env[\"PRISM_TEAM\"] ?? \"\";\n this.environment = options.environment ?? process.env[\"PRISM_ENVIRONMENT\"] ?? \"production\";\n this.sessionId = options.sessionId ?? crypto.randomUUID();\n this.sharedOpts = options;\n\n if (!this.key) {\n console.warn(\"[prism-mcp] PrismSession: PRISM_API_KEY not set — observability disabled.\");\n }\n }\n\n /**\n * Create a PrismMCP instance bound to this session.\n * All servers created from the same session share the same session_id.\n */\n createServer(perServer: PerServerOptions = {}): PrismMCP {\n return new PrismMCP({\n prismKey: this.key,\n project: this.project,\n team: this.team,\n environment: this.environment,\n sessionId: this.sessionId,\n sessionBudgetUsd: this.sharedOpts.sessionBudgetUsd,\n maxToolCallsPerSession: this.sharedOpts.maxToolCallsPerSession,\n captureInputs: this.sharedOpts.captureInputs,\n captureOutputs: this.sharedOpts.captureOutputs,\n redactKeys: this.sharedOpts.redactKeys,\n ingestUrl: this.sharedOpts.ingestUrl,\n // per-server overrides\n serverName: perServer.serverName,\n });\n }\n\n /**\n * Returns options to pass to the Prism LLM SDK clients (@prism-llm-labs/sdk)\n * so that LLM completions share this session's session_id.\n *\n * import { OpenAI } from \"@prism-llm-labs/sdk\";\n * const openai = new OpenAI(session.toLLMOptions());\n */\n toLLMOptions(): {\n prismKey: string;\n project: string;\n team: string;\n environment: string;\n sessionId: string;\n } {\n return {\n prismKey: this.key,\n project: this.project,\n team: this.team,\n environment: this.environment,\n sessionId: this.sessionId,\n };\n }\n}\n"],"mappings":"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","names":["orgFromKey"]}
|