@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.
@@ -0,0 +1,352 @@
1
+ /**
2
+ * PrismMCP — instruments any MCP Server with full observability:
3
+ * - tools/call → wrapToolCall() / patchHandler()
4
+ * - resources/read → wrapResourceRead() / patchResourceHandler()
5
+ * - prompts/get → wrapPromptGet() / patchPromptHandler()
6
+ * - sampling/createMessage → wrapSamplingHandler()
7
+ *
8
+ * Features:
9
+ * - Session budget circuit breaker (throws before execution if over budget)
10
+ * - Tool loop detection (throws if max_tool_calls_per_session exceeded)
11
+ * - Opt-in I/O capture (captureInputs / captureOutputs)
12
+ * - Streaming tool latency: correctly measures time-to-stream-end, not first-chunk
13
+ */
14
+
15
+ import type { McpPrimitiveType, PrismMcpOptions } from "./types";
16
+ import { McpEventTracker } from "./tracker";
17
+ import { SessionBudgetChecker } from "./budget";
18
+ import { lookupToolCost } from "./pricing";
19
+
20
+ // ── Private helpers ────────────────────────────────────────────────────────────
21
+
22
+ function orgFromKey(key: string): string {
23
+ const parts = key.split("_");
24
+ return parts.length >= 4 ? (parts[2] ?? "") : "";
25
+ }
26
+
27
+ const DEFAULT_REDACT_KEYS = ["password", "token", "key", "secret", "api_key", "authorization"];
28
+
29
+ function redactObject(
30
+ obj: unknown,
31
+ redactKeys: string[],
32
+ ): unknown {
33
+ if (typeof obj !== "object" || obj === null) return obj;
34
+ if (Array.isArray(obj)) return obj.map((v) => redactObject(v, redactKeys));
35
+ const out: Record<string, unknown> = {};
36
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
37
+ out[k] = redactKeys.some((r) => k.toLowerCase().includes(r.toLowerCase()))
38
+ ? "[REDACTED]"
39
+ : redactObject(v, redactKeys);
40
+ }
41
+ return out;
42
+ }
43
+
44
+ function truncate(s: string, max: number): string {
45
+ return s.length <= max ? s : s.slice(0, max) + "…";
46
+ }
47
+
48
+ function safeJson(val: unknown, redactKeys: string[], maxLen: number): string {
49
+ try {
50
+ return truncate(JSON.stringify(redactObject(val, redactKeys)), maxLen);
51
+ } catch {
52
+ return "[unserializable]";
53
+ }
54
+ }
55
+
56
+ // ── Streaming proxy (Epic 6) ───────────────────────────────────────────────────
57
+
58
+ function isAsyncIterable(val: unknown): val is AsyncIterable<unknown> {
59
+ return val != null &&
60
+ typeof (val as AsyncIterable<unknown>)[Symbol.asyncIterator] === "function";
61
+ }
62
+
63
+ /**
64
+ * Wraps an async iterable so that `onEnd` is called with the total elapsed ms
65
+ * when the stream is fully consumed or throws. All values are forwarded unchanged.
66
+ */
67
+ async function* proxyAsyncIterable<T>(
68
+ source: AsyncIterable<T>,
69
+ start: number,
70
+ onEnd: (latencyMs: number, threw: boolean) => void,
71
+ ): AsyncGenerator<T> {
72
+ let threw = false;
73
+ try {
74
+ for await (const value of source) {
75
+ yield value;
76
+ }
77
+ } catch (err) {
78
+ threw = true;
79
+ throw err;
80
+ } finally {
81
+ onEnd(Date.now() - start, threw);
82
+ }
83
+ }
84
+
85
+ // ── WrapContext — passed to the fn() callback for actual cost self-reporting ──
86
+
87
+ export class WrapContext {
88
+ /** @internal - read by _wrap() after fn() completes */
89
+ _actualCostUsd: number | null = null;
90
+
91
+ /**
92
+ * Override the catalog-estimated tool cost with the real billing figure.
93
+ * Call this inside your tool handler when you have access to actual cost data
94
+ * (e.g. from AWS SDK response metadata, a billing header, or a usage API).
95
+ *
96
+ * @example
97
+ * await prismMcp.wrapToolCall("invoke_lambda", async (ctx) => {
98
+ * const res = await lambda.invoke({ FunctionName: "fn", Payload: payload });
99
+ * const billedMs = res.$metadata.httpHeaders?.["x-amz-billed-duration-ms"] ?? "0";
100
+ * ctx.reportActualCost(parseInt(billedMs) * 0.000016667 / 1000);
101
+ * return res;
102
+ * });
103
+ */
104
+ reportActualCost(usd: number): void {
105
+ this._actualCostUsd = usd;
106
+ }
107
+ }
108
+
109
+ // ── Main class ────────────────────────────────────────────────────────────────
110
+
111
+ export class PrismMCP {
112
+ private readonly tracker: McpEventTracker;
113
+ private readonly budget: SessionBudgetChecker;
114
+ private readonly redactKeys: string[];
115
+ private readonly opts: Required<Pick<PrismMcpOptions,
116
+ "project" | "team" | "environment" | "sessionId" | "serverName" |
117
+ "captureInputs" | "captureOutputs">> &
118
+ Pick<PrismMcpOptions, "sessionBudgetUsd" | "maxToolCallsPerSession">;
119
+
120
+ constructor(options: PrismMcpOptions = {}) {
121
+ const key = options.prismKey ?? process.env["PRISM_API_KEY"] ?? "";
122
+ if (!key) {
123
+ console.warn("[prism-mcp] PRISM_API_KEY not set — MCP observability disabled.");
124
+ }
125
+
126
+ this.opts = {
127
+ project: options.project ?? process.env["PRISM_PROJECT"] ?? "",
128
+ team: options.team ?? process.env["PRISM_TEAM"] ?? "",
129
+ environment: options.environment ?? process.env["PRISM_ENVIRONMENT"] ?? "production",
130
+ sessionId: options.sessionId ?? crypto.randomUUID(),
131
+ serverName: options.serverName ?? "mcp-server",
132
+ sessionBudgetUsd: options.sessionBudgetUsd,
133
+ maxToolCallsPerSession: options.maxToolCallsPerSession,
134
+ captureInputs: options.captureInputs ?? false,
135
+ captureOutputs: options.captureOutputs ?? false,
136
+ };
137
+ this.redactKeys = options.redactKeys ?? DEFAULT_REDACT_KEYS;
138
+ this.tracker = new McpEventTracker(key, this.opts.serverName, options.ingestUrl);
139
+ this.budget = new SessionBudgetChecker(orgFromKey(key));
140
+ }
141
+
142
+ // ── Core primitive wrapper ─────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Internal wrapper used by all four MCP primitives.
146
+ * Handles: budget check, I/O capture, streaming proxy, fire-and-forget telemetry,
147
+ * and actual cost self-reporting via ctx.reportActualCost().
148
+ */
149
+ private async _wrap<T>(
150
+ primitiveType: McpPrimitiveType,
151
+ name: string,
152
+ fn: (ctx: WrapContext) => Promise<T>,
153
+ extra: {
154
+ llmRequestId?: string;
155
+ tags?: Record<string, string>;
156
+ downstreamResource?: string;
157
+ inputs?: Record<string, unknown>;
158
+ } = {},
159
+ ): Promise<T> {
160
+ // 1. Pre-call budget/loop guard
161
+ await this.budget.checkOrThrow(
162
+ this.opts.sessionId,
163
+ this.opts.sessionBudgetUsd,
164
+ this.opts.maxToolCallsPerSession,
165
+ );
166
+
167
+ const start = Date.now();
168
+ let status: "ok" | "error" | "timeout" = "ok";
169
+ let errorMsg = "";
170
+ let result: T;
171
+ const ctx = new WrapContext();
172
+
173
+ // Build tags — I/O capture applied here
174
+ const eventTags: Record<string, string> = { ...extra.tags };
175
+ if (this.opts.captureInputs && extra.inputs != null) {
176
+ eventTags["tool_input"] = safeJson(extra.inputs, this.redactKeys, 1000);
177
+ }
178
+
179
+ // Cost lookup — overridden by ctx.reportActualCost() if called during fn()
180
+ const estimatedCost = primitiveType === "tool" ? lookupToolCost(name) : 0;
181
+
182
+ const fireEvent = (latencyMs: number, finalStatus: "ok" | "error" | "timeout") => {
183
+ const actualCostUsd = ctx._actualCostUsd;
184
+ this.tracker.capture({
185
+ timestamp: new Date().toISOString().replace("T", " ").slice(0, 23),
186
+ session_id: this.opts.sessionId,
187
+ project_id: this.opts.project,
188
+ team_id: this.opts.team,
189
+ user_id: "",
190
+ environment: this.opts.environment,
191
+ tool_name: name,
192
+ downstream_resource: extra.downstreamResource ?? "",
193
+ execution_latency_ms: latencyMs,
194
+ tool_cost_usd: actualCostUsd ?? estimatedCost,
195
+ cost_status: actualCostUsd != null ? "actual" : "estimated",
196
+ status: finalStatus,
197
+ error_message: errorMsg,
198
+ llm_request_id: extra.llmRequestId ?? "",
199
+ primitive_type: primitiveType,
200
+ tags: eventTags,
201
+ }).catch(() => {});
202
+ };
203
+
204
+ try {
205
+ result = await fn(ctx);
206
+ } catch (err) {
207
+ status = "error";
208
+ errorMsg = err instanceof Error ? err.message : String(err);
209
+ fireEvent(Date.now() - start, status);
210
+ throw err;
211
+ }
212
+
213
+ // Capture output if requested (non-streaming path)
214
+ if (this.opts.captureOutputs && result != null && !isAsyncIterable(result)) {
215
+ eventTags["tool_output"] = safeJson(result, this.redactKeys, 1000);
216
+ }
217
+
218
+ // Streaming path — defer telemetry until stream is exhausted
219
+ if (isAsyncIterable(result)) {
220
+ return proxyAsyncIterable(
221
+ result as AsyncIterable<unknown>,
222
+ start,
223
+ (latencyMs, threw) => {
224
+ fireEvent(latencyMs, threw ? "error" : "ok");
225
+ },
226
+ ) as unknown as T;
227
+ }
228
+
229
+ // Non-streaming path — fire immediately
230
+ fireEvent(Date.now() - start, status);
231
+ return result;
232
+ }
233
+
234
+ // ── Public API: tools ──────────────────────────────────────────────────────
235
+
236
+ /**
237
+ * Wrap a tools/call execution with Prism instrumentation.
238
+ */
239
+ async wrapToolCall<T>(
240
+ toolName: string,
241
+ fn: (ctx: WrapContext) => Promise<T>,
242
+ extra: {
243
+ llmRequestId?: string;
244
+ tags?: Record<string, string>;
245
+ downstreamResource?: string;
246
+ /** Raw tool arguments — captured if captureInputs: true */
247
+ inputs?: Record<string, unknown>;
248
+ } = {},
249
+ ): Promise<T> {
250
+ return this._wrap("tool", toolName, fn, extra);
251
+ }
252
+
253
+ /**
254
+ * Drop-in patch for MCP SDK's CallToolRequestSchema handler.
255
+ * Automatically passes req.params.arguments as inputs when captureInputs: true.
256
+ * ctx is available for reportActualCost() inside the handler.
257
+ */
258
+ patchHandler<TReq extends { params: { name: string; arguments?: Record<string, unknown> } }, TRes>(
259
+ handler: (req: TReq, ctx: WrapContext) => Promise<TRes>,
260
+ ): (req: TReq) => Promise<TRes> {
261
+ const self = this;
262
+ return async function patchedHandler(req: TReq): Promise<TRes> {
263
+ return self.wrapToolCall(
264
+ req.params.name,
265
+ (ctx) => handler(req, ctx),
266
+ { inputs: req.params.arguments },
267
+ );
268
+ };
269
+ }
270
+
271
+ // ── Public API: resources ──────────────────────────────────────────────────
272
+
273
+ /**
274
+ * Wrap a resources/read execution with Prism instrumentation.
275
+ *
276
+ * @param resourceUri - The resource URI being read (e.g. "file:///path/to/file")
277
+ */
278
+ async wrapResourceRead<T>(
279
+ resourceUri: string,
280
+ fn: (ctx: WrapContext) => Promise<T>,
281
+ extra: {
282
+ llmRequestId?: string;
283
+ tags?: Record<string, string>;
284
+ } = {},
285
+ ): Promise<T> {
286
+ return this._wrap("resource", resourceUri, fn, extra);
287
+ }
288
+
289
+ patchResourceHandler<TReq extends { params: { uri: string } }, TRes>(
290
+ handler: (req: TReq, ctx: WrapContext) => Promise<TRes>,
291
+ ): (req: TReq) => Promise<TRes> {
292
+ const self = this;
293
+ return async function patchedResourceHandler(req: TReq): Promise<TRes> {
294
+ return self.wrapResourceRead(req.params.uri, (ctx) => handler(req, ctx));
295
+ };
296
+ }
297
+
298
+ // ── Public API: prompts ────────────────────────────────────────────────────
299
+
300
+ async wrapPromptGet<T>(
301
+ promptName: string,
302
+ fn: (ctx: WrapContext) => Promise<T>,
303
+ extra: {
304
+ llmRequestId?: string;
305
+ tags?: Record<string, string>;
306
+ } = {},
307
+ ): Promise<T> {
308
+ return this._wrap("prompt", promptName, fn, extra);
309
+ }
310
+
311
+ patchPromptHandler<TReq extends { params: { name: string } }, TRes>(
312
+ handler: (req: TReq, ctx: WrapContext) => Promise<TRes>,
313
+ ): (req: TReq) => Promise<TRes> {
314
+ const self = this;
315
+ return async function patchedPromptHandler(req: TReq): Promise<TRes> {
316
+ return self.wrapPromptGet(req.params.name, (ctx) => handler(req, ctx));
317
+ };
318
+ }
319
+
320
+ // ── Public API: sampling ───────────────────────────────────────────────────
321
+
322
+ /**
323
+ * Wrap the MCP client's sampling/createMessage callback so LLM calls
324
+ * requested by the MCP server appear in the session timeline.
325
+ *
326
+ * Usage with @modelcontextprotocol/sdk:
327
+ * client.setRequestHandler(
328
+ * CreateMessageRequestSchema,
329
+ * prismMcp.wrapSamplingHandler(async (req) => {
330
+ * const res = await openai.chat.completions.create({ ... });
331
+ * return { role: "assistant", content: [{ type: "text", text: res.choices[0].message.content }] };
332
+ * })
333
+ * );
334
+ */
335
+ wrapSamplingHandler<
336
+ TReq extends { params?: { modelPreferences?: { hints?: Array<{ name?: string }> } } },
337
+ TRes,
338
+ >(
339
+ handler: (req: TReq) => Promise<TRes>,
340
+ ): (req: TReq) => Promise<TRes> {
341
+ const self = this;
342
+ return async function patchedSamplingHandler(req: TReq): Promise<TRes> {
343
+ // Use model hint as the "tool_name" for display in the session timeline
344
+ const modelHint =
345
+ req.params?.modelPreferences?.hints?.[0]?.name ?? "sampling";
346
+ return self._wrap("sampling", modelHint, (_ctx) => handler(req));
347
+ };
348
+ }
349
+
350
+ /** The session_id assigned to this instance (useful for logging). */
351
+ get sessionId(): string { return this.opts.sessionId; }
352
+ }
package/src/session.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * PrismSession — shared session context for multi-server agent runs.
3
+ *
4
+ * Creates a single session_id that is automatically threaded through:
5
+ * - Multiple PrismMCP server instances (files, database, search, etc.)
6
+ * - LLM SDK clients (OpenAI, Anthropic) via toLLMOptions()
7
+ *
8
+ * This ensures every LLM call and every tool/resource/prompt call in one
9
+ * agent run appears under the same session in /dashboard/sessions/[id].
10
+ *
11
+ * Usage:
12
+ * const session = new PrismSession({
13
+ * prismKey: process.env.PRISM_API_KEY,
14
+ * project: "customer-support",
15
+ * sessionBudgetUsd: 2.00, // hard cap for the whole agent run
16
+ * });
17
+ *
18
+ * // MCP servers — all share session.sessionId automatically
19
+ * const filesMcp = session.createServer({ serverName: "files" });
20
+ * const dbMcp = session.createServer({ serverName: "database" });
21
+ * const searchMcp = session.createServer({ serverName: "search" });
22
+ *
23
+ * // LLM SDK client — same session_id
24
+ * import { OpenAI } from "@prism-llm-labs/sdk";
25
+ * const openai = new OpenAI(session.toLLMOptions());
26
+ */
27
+
28
+ import { PrismMCP } from "./prism-mcp";
29
+ import type { PrismMcpOptions } from "./types";
30
+
31
+ export interface PrismSessionOptions {
32
+ /** Prism API key — or set PRISM_API_KEY env var */
33
+ prismKey?: string;
34
+ /** Project ID for cost attribution */
35
+ project?: string;
36
+ /** Team attribution tag */
37
+ team?: string;
38
+ /** "production" | "staging" | "development" */
39
+ environment?: string;
40
+ /**
41
+ * Explicit session ID. Auto-generated UUID if omitted.
42
+ * Pass an explicit ID when the session ID is determined by an external
43
+ * orchestrator (e.g. a LangGraph run ID, a task queue job ID).
44
+ */
45
+ sessionId?: string;
46
+ /** Hard cost cap across ALL servers in this session */
47
+ sessionBudgetUsd?: number;
48
+ /** Hard tool-call cap across ALL servers in this session */
49
+ maxToolCallsPerSession?: number;
50
+ /** Opt-in I/O capture for all servers created from this session */
51
+ captureInputs?: boolean;
52
+ captureOutputs?: boolean;
53
+ redactKeys?: string[];
54
+ /** Override ingest URL (for testing) */
55
+ ingestUrl?: string;
56
+ }
57
+
58
+ /** Options for individual servers within a session */
59
+ export type PerServerOptions = Omit<
60
+ PrismMcpOptions,
61
+ "prismKey" | "project" | "team" | "environment" | "sessionId" |
62
+ "sessionBudgetUsd" | "maxToolCallsPerSession" | "captureInputs" |
63
+ "captureOutputs" | "redactKeys" | "ingestUrl"
64
+ >;
65
+
66
+ export class PrismSession {
67
+ readonly sessionId: string;
68
+
69
+ private readonly key: string;
70
+ private readonly project: string;
71
+ private readonly team: string;
72
+ private readonly environment: string;
73
+ private readonly sharedOpts: PrismSessionOptions;
74
+
75
+ constructor(options: PrismSessionOptions = {}) {
76
+ this.key = options.prismKey ?? process.env["PRISM_API_KEY"] ?? "";
77
+ this.project = options.project ?? process.env["PRISM_PROJECT"] ?? "";
78
+ this.team = options.team ?? process.env["PRISM_TEAM"] ?? "";
79
+ this.environment = options.environment ?? process.env["PRISM_ENVIRONMENT"] ?? "production";
80
+ this.sessionId = options.sessionId ?? crypto.randomUUID();
81
+ this.sharedOpts = options;
82
+
83
+ if (!this.key) {
84
+ console.warn("[prism-mcp] PrismSession: PRISM_API_KEY not set — observability disabled.");
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create a PrismMCP instance bound to this session.
90
+ * All servers created from the same session share the same session_id.
91
+ */
92
+ createServer(perServer: PerServerOptions = {}): PrismMCP {
93
+ return new PrismMCP({
94
+ prismKey: this.key,
95
+ project: this.project,
96
+ team: this.team,
97
+ environment: this.environment,
98
+ sessionId: this.sessionId,
99
+ sessionBudgetUsd: this.sharedOpts.sessionBudgetUsd,
100
+ maxToolCallsPerSession: this.sharedOpts.maxToolCallsPerSession,
101
+ captureInputs: this.sharedOpts.captureInputs,
102
+ captureOutputs: this.sharedOpts.captureOutputs,
103
+ redactKeys: this.sharedOpts.redactKeys,
104
+ ingestUrl: this.sharedOpts.ingestUrl,
105
+ // per-server overrides
106
+ serverName: perServer.serverName,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Returns options to pass to the Prism LLM SDK clients (@prism-llm-labs/sdk)
112
+ * so that LLM completions share this session's session_id.
113
+ *
114
+ * import { OpenAI } from "@prism-llm-labs/sdk";
115
+ * const openai = new OpenAI(session.toLLMOptions());
116
+ */
117
+ toLLMOptions(): {
118
+ prismKey: string;
119
+ project: string;
120
+ team: string;
121
+ environment: string;
122
+ sessionId: string;
123
+ } {
124
+ return {
125
+ prismKey: this.key,
126
+ project: this.project,
127
+ team: this.team,
128
+ environment: this.environment,
129
+ sessionId: this.sessionId,
130
+ };
131
+ }
132
+ }
package/src/tracker.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { McpEvent } from "./types";
2
+
3
+ function defaultIngestUrl(): string {
4
+ const appUrl = (
5
+ process.env["PRISM_APP_URL"] ??
6
+ process.env["NEXT_PUBLIC_APP_URL"] ??
7
+ "https://useprism.dev"
8
+ ).replace(/\/$/, "");
9
+ return `${appUrl}/api/mcp/ingest`;
10
+ }
11
+
12
+ function orgFromKey(key: string): string {
13
+ const parts = key.split("_");
14
+ return parts.length >= 4 ? (parts[2] ?? "") : "";
15
+ }
16
+
17
+ export class McpEventTracker {
18
+ private readonly key: string;
19
+ private readonly ingestUrl: string;
20
+ private readonly serverName: string;
21
+ private readonly orgId: string;
22
+
23
+ constructor(key: string, serverName: string, ingestUrl?: string) {
24
+ this.key = key;
25
+ this.ingestUrl = ingestUrl ?? defaultIngestUrl();
26
+ this.serverName = serverName;
27
+ this.orgId = orgFromKey(key);
28
+ }
29
+
30
+ async capture(event: Omit<McpEvent, "event_id" | "org_id" | "mcp_server_name">): Promise<void> {
31
+ try {
32
+ const full: McpEvent = {
33
+ ...event,
34
+ event_id: crypto.randomUUID(),
35
+ org_id: this.orgId,
36
+ mcp_server_name: this.serverName,
37
+ };
38
+
39
+ const res = await fetch(this.ingestUrl, {
40
+ method: "POST",
41
+ headers: {
42
+ Authorization: `Bearer ${this.key}`,
43
+ "Content-Type": "application/json",
44
+ },
45
+ body: JSON.stringify({ events: [full] }),
46
+ });
47
+
48
+ if (!res.ok && res.status !== 422) {
49
+ // Silently ignore — observability must never break the agent
50
+ console.warn(`[prism-mcp] Ingest returned ${res.status}`);
51
+ }
52
+ } catch {
53
+ // Never propagate
54
+ }
55
+ }
56
+ }
package/src/types.ts ADDED
@@ -0,0 +1,90 @@
1
+ export type McpPrimitiveType = "tool" | "resource" | "prompt" | "sampling";
2
+
3
+ export interface McpEvent {
4
+ event_id: string;
5
+ timestamp: string;
6
+ session_id: string;
7
+ org_id: string;
8
+ project_id: string;
9
+ team_id: string;
10
+ user_id: string;
11
+ environment: string;
12
+ mcp_server_name: string;
13
+ /** tool name, resource URI, prompt name, or model hint for sampling */
14
+ tool_name: string;
15
+ downstream_resource: string;
16
+ execution_latency_ms: number;
17
+ tool_cost_usd: number;
18
+ status: "ok" | "error" | "timeout";
19
+ error_message: string;
20
+ llm_request_id: string;
21
+ primitive_type: McpPrimitiveType;
22
+ /**
23
+ * Whether tool_cost_usd is an estimate from the built-in catalog ("estimated")
24
+ * or a real figure provided by the tool via ctx.reportActualCost() ("actual").
25
+ */
26
+ cost_status: "estimated" | "actual";
27
+ tags: Record<string, string>;
28
+ }
29
+
30
+ export interface PrismMcpOptions {
31
+ /** Prism API key — or set PRISM_API_KEY env var */
32
+ prismKey?: string;
33
+ /** Project ID for cost attribution */
34
+ project?: string;
35
+ /** Team attribution tag */
36
+ team?: string;
37
+ /** "production" | "staging" | "development" */
38
+ environment?: string;
39
+ /** Explicit session ID. Auto-generated UUID if omitted. */
40
+ sessionId?: string;
41
+ /** MCP server name shown in the dashboard */
42
+ serverName?: string;
43
+ /** Override ingest URL (for testing) */
44
+ ingestUrl?: string;
45
+ /**
46
+ * Session budget in USD. Tool/resource/prompt calls are blocked when the
47
+ * combined session cost exceeds this value.
48
+ */
49
+ sessionBudgetUsd?: number;
50
+ /**
51
+ * Maximum MCP primitive calls per session. Blocks further calls when exceeded.
52
+ * Loop detection guard — default unlimited.
53
+ */
54
+ maxToolCallsPerSession?: number;
55
+ /**
56
+ * Log call arguments into tags['tool_input'] (truncated to 1000 chars).
57
+ * Opt-in only — disabled by default for privacy.
58
+ */
59
+ captureInputs?: boolean;
60
+ /**
61
+ * Log call results into tags['tool_output'] (truncated to 1000 chars).
62
+ * Opt-in only — disabled by default for privacy.
63
+ */
64
+ captureOutputs?: boolean;
65
+ /**
66
+ * Keys to redact from captured inputs/outputs.
67
+ * Default: ['password', 'token', 'key', 'secret', 'api_key', 'authorization']
68
+ */
69
+ redactKeys?: string[];
70
+ }
71
+
72
+ export class PrismSessionBudgetExceededError extends Error {
73
+ constructor(sessionId: string, budgetUsd: number) {
74
+ super(
75
+ `[prism-mcp] Session budget of $${budgetUsd} exceeded for session ${sessionId}. ` +
76
+ `Tool call blocked to prevent runaway agent costs.`,
77
+ );
78
+ this.name = "PrismSessionBudgetExceededError";
79
+ }
80
+ }
81
+
82
+ export class PrismToolCallLimitError extends Error {
83
+ constructor(sessionId: string, limit: number) {
84
+ super(
85
+ `[prism-mcp] Tool call limit of ${limit} reached for session ${sessionId}. ` +
86
+ `Possible agent loop detected — tool call blocked.`,
87
+ );
88
+ this.name = "PrismToolCallLimitError";
89
+ }
90
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2020"],
7
+ "strict": true,
8
+ "declaration": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ });