@prism-llm-labs/mcp-sdk 0.2.0 → 0.3.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 CHANGED
@@ -90,6 +90,8 @@ declare class PrismToolCallLimitError extends Error {
90
90
  declare class WrapContext {
91
91
  /** @internal - read by _wrap() after fn() completes */
92
92
  _actualCostUsd: number | null;
93
+ /** @internal */
94
+ _downstreamResource: string | null;
93
95
  /**
94
96
  * Override the catalog-estimated tool cost with the real billing figure.
95
97
  * Call this inside your tool handler when you have access to actual cost data
@@ -104,6 +106,26 @@ declare class WrapContext {
104
106
  * });
105
107
  */
106
108
  reportActualCost(usd: number): void;
109
+ /**
110
+ * Set the downstream resource identifier for this call.
111
+ * Use this when the resource name is resolved inside the handler (e.g. after
112
+ * looking up which Pinecone index to query).
113
+ *
114
+ * Convention:
115
+ * - Pinecone: "pinecone:<index-name>" e.g. "pinecone:product-embeddings"
116
+ * - Qdrant: "qdrant:<collection>" e.g. "qdrant:support-docs"
117
+ * - Generic: just the provider name e.g. "weaviate", "redis"
118
+ *
119
+ * Overrides the `downstreamResource` option passed to wrapToolCall().
120
+ *
121
+ * @example
122
+ * await prismMcp.wrapToolCall("vector_search", async (ctx) => {
123
+ * const index = await resolveIndex(query);
124
+ * ctx.setDownstreamResource(`pinecone:${index}`);
125
+ * return pinecone.query({ index, vector });
126
+ * });
127
+ */
128
+ setDownstreamResource(resource: string): void;
107
129
  }
108
130
  declare class PrismMCP {
109
131
  private readonly tracker;
@@ -300,6 +322,19 @@ declare class SessionBudgetChecker {
300
322
  /**
301
323
  * Built-in tool cost estimates (USD per call).
302
324
  * Mirrors apps/web/lib/pricing/tool-catalog.ts — keep in sync.
325
+ *
326
+ * Pricing basis:
327
+ * - Filesystem / Git: trivial I/O; estimated at $0.01/1K ops
328
+ * - GitHub API: free tier 5 000 req/hr; charged via Actions minutes at ~$0.0003/req
329
+ * - Slack / email: negligible API cost; estimated at overhead cost
330
+ * - Databases: typical query cost on managed DB (Neon/RDS) per operation
331
+ * - HTTP / browser: outbound network + optional headless browser compute
332
+ * - AWS: official published rates (Lambda $0.0000002/req, S3 $0.004/10K GET)
333
+ * - Search APIs: published per-query pricing
334
+ * - Code execution: E2B/code_interpreter published rates
335
+ *
336
+ * Prefix wildcards ("github_*") match any tool whose name starts with that prefix.
337
+ * lookupToolCost() tries exact → longest-prefix → 0.
303
338
  */
304
339
  /**
305
340
  * Look up estimated cost for a tool name.
package/dist/index.d.ts CHANGED
@@ -90,6 +90,8 @@ declare class PrismToolCallLimitError extends Error {
90
90
  declare class WrapContext {
91
91
  /** @internal - read by _wrap() after fn() completes */
92
92
  _actualCostUsd: number | null;
93
+ /** @internal */
94
+ _downstreamResource: string | null;
93
95
  /**
94
96
  * Override the catalog-estimated tool cost with the real billing figure.
95
97
  * Call this inside your tool handler when you have access to actual cost data
@@ -104,6 +106,26 @@ declare class WrapContext {
104
106
  * });
105
107
  */
106
108
  reportActualCost(usd: number): void;
109
+ /**
110
+ * Set the downstream resource identifier for this call.
111
+ * Use this when the resource name is resolved inside the handler (e.g. after
112
+ * looking up which Pinecone index to query).
113
+ *
114
+ * Convention:
115
+ * - Pinecone: "pinecone:<index-name>" e.g. "pinecone:product-embeddings"
116
+ * - Qdrant: "qdrant:<collection>" e.g. "qdrant:support-docs"
117
+ * - Generic: just the provider name e.g. "weaviate", "redis"
118
+ *
119
+ * Overrides the `downstreamResource` option passed to wrapToolCall().
120
+ *
121
+ * @example
122
+ * await prismMcp.wrapToolCall("vector_search", async (ctx) => {
123
+ * const index = await resolveIndex(query);
124
+ * ctx.setDownstreamResource(`pinecone:${index}`);
125
+ * return pinecone.query({ index, vector });
126
+ * });
127
+ */
128
+ setDownstreamResource(resource: string): void;
107
129
  }
108
130
  declare class PrismMCP {
109
131
  private readonly tracker;
@@ -300,6 +322,19 @@ declare class SessionBudgetChecker {
300
322
  /**
301
323
  * Built-in tool cost estimates (USD per call).
302
324
  * Mirrors apps/web/lib/pricing/tool-catalog.ts — keep in sync.
325
+ *
326
+ * Pricing basis:
327
+ * - Filesystem / Git: trivial I/O; estimated at $0.01/1K ops
328
+ * - GitHub API: free tier 5 000 req/hr; charged via Actions minutes at ~$0.0003/req
329
+ * - Slack / email: negligible API cost; estimated at overhead cost
330
+ * - Databases: typical query cost on managed DB (Neon/RDS) per operation
331
+ * - HTTP / browser: outbound network + optional headless browser compute
332
+ * - AWS: official published rates (Lambda $0.0000002/req, S3 $0.004/10K GET)
333
+ * - Search APIs: published per-query pricing
334
+ * - Code execution: E2B/code_interpreter published rates
335
+ *
336
+ * Prefix wildcards ("github_*") match any tool whose name starts with that prefix.
337
+ * lookupToolCost() tries exact → longest-prefix → 0.
303
338
  */
304
339
  /**
305
340
  * Look up estimated cost for a tool name.
package/dist/index.js CHANGED
@@ -129,27 +129,150 @@ var SessionBudgetChecker = class {
129
129
 
130
130
  // src/pricing.ts
131
131
  var BUILT_IN = {
132
- // Vector DBs
132
+ // ── Filesystem & local I/O ─────────────────────────────────────────────────
133
+ read_file: 1e-5,
134
+ write_file: 5e-5,
135
+ create_file: 5e-5,
136
+ delete_file: 2e-5,
137
+ list_directory: 1e-5,
138
+ move_file: 2e-5,
139
+ copy_file: 2e-5,
140
+ search_files: 3e-5,
141
+ get_file_info: 5e-6,
142
+ read_multiple_files: 3e-5,
143
+ // ── Git operations ─────────────────────────────────────────────────────────
144
+ git_status: 5e-6,
145
+ git_diff: 1e-5,
146
+ git_commit: 5e-5,
147
+ git_push: 2e-4,
148
+ git_pull: 1e-4,
149
+ git_clone: 5e-4,
150
+ git_log: 1e-5,
151
+ git_branch: 5e-6,
152
+ git_checkout: 2e-5,
153
+ git_merge: 1e-4,
154
+ // ── GitHub API ────────────────────────────────────────────────────────────
155
+ create_issue: 3e-4,
156
+ update_issue: 3e-4,
157
+ close_issue: 2e-4,
158
+ list_issues: 1e-4,
159
+ get_issue: 1e-4,
160
+ create_pull_request: 3e-4,
161
+ merge_pull_request: 5e-4,
162
+ list_pull_requests: 1e-4,
163
+ search_code: 1e-3,
164
+ search_repositories: 5e-4,
165
+ get_file_contents: 1e-4,
166
+ create_or_update_file: 3e-4,
167
+ fork_repository: 5e-4,
168
+ create_repository: 5e-4,
169
+ push_files: 3e-4,
170
+ "github_*": 2e-4,
171
+ // wildcard fallback for any github_ prefixed tool
172
+ // ── Communication — Slack ─────────────────────────────────────────────────
173
+ slack_post_message: 1e-4,
174
+ slack_reply_to_thread: 1e-4,
175
+ slack_add_reaction: 5e-5,
176
+ slack_get_channel_info: 5e-5,
177
+ slack_list_channels: 8e-5,
178
+ slack_get_users: 1e-4,
179
+ slack_upload_file: 2e-4,
180
+ "slack_*": 8e-5,
181
+ // ── Communication — email / other ─────────────────────────────────────────
182
+ send_email: 1e-3,
183
+ send_slack_message: 1e-4,
184
+ // ── Databases ─────────────────────────────────────────────────────────────
185
+ postgres_query: 5e-4,
186
+ postgres_execute: 1e-3,
187
+ postgres_insert: 1e-3,
188
+ postgres_update: 1e-3,
189
+ postgres_delete: 1e-3,
190
+ "postgres_*": 5e-4,
191
+ mysql_query: 5e-4,
192
+ "mysql_*": 5e-4,
193
+ mongodb_find: 2e-4,
194
+ mongodb_insert: 5e-4,
195
+ "mongodb_*": 3e-4,
196
+ redis_get: 5e-5,
197
+ redis_set: 1e-4,
198
+ "redis_*": 8e-5,
199
+ sqlite_query: 5e-5,
200
+ // ── HTTP / REST ───────────────────────────────────────────────────────────
201
+ http_get: 2e-4,
202
+ http_post: 3e-4,
203
+ http_put: 3e-4,
204
+ http_delete: 2e-4,
205
+ http_fetch: 2e-4,
206
+ fetch_url: 2e-4,
207
+ make_api_request: 3e-4,
208
+ "http_*": 2e-4,
209
+ // ── Browser / web automation ──────────────────────────────────────────────
210
+ browser_navigate: 2e-3,
211
+ browser_screenshot: 3e-3,
212
+ browser_click: 1e-3,
213
+ browser_type: 1e-3,
214
+ browser_scroll: 5e-4,
215
+ browser_get_text: 1e-3,
216
+ browser_fill_form: 2e-3,
217
+ browser_wait: 5e-4,
218
+ puppeteer_navigate: 2e-3,
219
+ playwright_goto: 2e-3,
220
+ "browser_*": 2e-3,
221
+ "puppeteer_*": 2e-3,
222
+ "playwright_*": 2e-3,
223
+ // ── Project management — Jira ─────────────────────────────────────────────
224
+ create_jira_issue: 3e-4,
225
+ update_jira_issue: 3e-4,
226
+ search_jira_issues: 2e-4,
227
+ get_jira_issue: 1e-4,
228
+ add_jira_comment: 2e-4,
229
+ "jira_*": 2e-4,
230
+ // ── Project management — Linear / Notion / Asana ─────────────────────────
231
+ create_linear_issue: 3e-4,
232
+ "linear_*": 2e-4,
233
+ notion_create_page: 3e-4,
234
+ notion_update_page: 3e-4,
235
+ notion_search: 2e-4,
236
+ "notion_*": 2e-4,
237
+ create_asana_task: 3e-4,
238
+ "asana_*": 2e-4,
239
+ // ── Vector DBs ────────────────────────────────────────────────────────────
133
240
  pinecone_query: 1e-6,
134
241
  pinecone_upsert: 2e-6,
135
242
  weaviate_query: 8e-7,
136
243
  qdrant_search: 5e-7,
137
- // AWS
244
+ chroma_query: 5e-7,
245
+ // ── AWS ───────────────────────────────────────────────────────────────────
138
246
  lambda_invoke: 2e-7,
139
247
  s3_get_object: 4e-7,
140
248
  s3_put_object: 5e-6,
249
+ s3_list_objects: 5e-6,
141
250
  dynamodb_get_item: 25e-8,
142
251
  dynamodb_put_item: 125e-8,
143
- // Search / web
252
+ dynamodb_query: 125e-8,
253
+ sqs_send_message: 4e-7,
254
+ sns_publish: 5e-7,
255
+ "aws_*": 1e-6,
256
+ // ── Search / web ──────────────────────────────────────────────────────────
144
257
  brave_search: 3e-6,
145
258
  serper_search: 1e-6,
146
259
  tavily_search: 2e-6,
147
260
  exa_search: 5e-6,
148
- // Code execution
261
+ google_search: 5e-6,
262
+ bing_search: 3e-6,
263
+ duckduckgo_search: 1e-6,
264
+ web_search: 2e-6,
265
+ // ── Code execution ────────────────────────────────────────────────────────
149
266
  e2b_run_code: 14e-6,
150
267
  code_interpreter: 14e-6,
151
- // Communication
152
- send_email: 1e-6
268
+ bash_execute: 1e-3,
269
+ run_command: 1e-3,
270
+ run_terminal_command: 1e-3,
271
+ docker_run: 5e-3,
272
+ docker_exec: 2e-3,
273
+ // ── AI / embeddings ───────────────────────────────────────────────────────
274
+ generate_embedding: 1e-5,
275
+ openai_embedding: 1e-5
153
276
  };
154
277
  function lookupToolCost(toolName, overrides = {}) {
155
278
  const all = { ...BUILT_IN, ...overrides };
@@ -203,6 +326,8 @@ var WrapContext = class {
203
326
  constructor() {
204
327
  /** @internal - read by _wrap() after fn() completes */
205
328
  this._actualCostUsd = null;
329
+ /** @internal */
330
+ this._downstreamResource = null;
206
331
  }
207
332
  /**
208
333
  * Override the catalog-estimated tool cost with the real billing figure.
@@ -220,6 +345,28 @@ var WrapContext = class {
220
345
  reportActualCost(usd) {
221
346
  this._actualCostUsd = usd;
222
347
  }
348
+ /**
349
+ * Set the downstream resource identifier for this call.
350
+ * Use this when the resource name is resolved inside the handler (e.g. after
351
+ * looking up which Pinecone index to query).
352
+ *
353
+ * Convention:
354
+ * - Pinecone: "pinecone:<index-name>" e.g. "pinecone:product-embeddings"
355
+ * - Qdrant: "qdrant:<collection>" e.g. "qdrant:support-docs"
356
+ * - Generic: just the provider name e.g. "weaviate", "redis"
357
+ *
358
+ * Overrides the `downstreamResource` option passed to wrapToolCall().
359
+ *
360
+ * @example
361
+ * await prismMcp.wrapToolCall("vector_search", async (ctx) => {
362
+ * const index = await resolveIndex(query);
363
+ * ctx.setDownstreamResource(`pinecone:${index}`);
364
+ * return pinecone.query({ index, vector });
365
+ * });
366
+ */
367
+ setDownstreamResource(resource) {
368
+ this._downstreamResource = resource;
369
+ }
223
370
  };
224
371
  var PrismMCP = class {
225
372
  constructor(options = {}) {
@@ -274,7 +421,7 @@ var PrismMCP = class {
274
421
  user_id: "",
275
422
  environment: this.opts.environment,
276
423
  tool_name: name,
277
- downstream_resource: extra.downstreamResource ?? "",
424
+ downstream_resource: ctx._downstreamResource ?? extra.downstreamResource ?? "",
278
425
  execution_latency_ms: latencyMs,
279
426
  tool_cost_usd: actualCostUsd ?? estimatedCost,
280
427
  cost_status: actualCostUsd != null ? "actual" : "estimated",
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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 * Pricing basis:\n * - Filesystem / Git: trivial I/O; estimated at $0.01/1K ops\n * - GitHub API: free tier 5 000 req/hr; charged via Actions minutes at ~$0.0003/req\n * - Slack / email: negligible API cost; estimated at overhead cost\n * - Databases: typical query cost on managed DB (Neon/RDS) per operation\n * - HTTP / browser: outbound network + optional headless browser compute\n * - AWS: official published rates (Lambda $0.0000002/req, S3 $0.004/10K GET)\n * - Search APIs: published per-query pricing\n * - Code execution: E2B/code_interpreter published rates\n *\n * Prefix wildcards (\"github_*\") match any tool whose name starts with that prefix.\n * lookupToolCost() tries exact → longest-prefix → 0.\n */\n\nconst BUILT_IN: Record<string, number> = {\n // ── Filesystem & local I/O ─────────────────────────────────────────────────\n read_file: 0.00001,\n write_file: 0.00005,\n create_file: 0.00005,\n delete_file: 0.00002,\n list_directory: 0.00001,\n move_file: 0.00002,\n copy_file: 0.00002,\n search_files: 0.00003,\n get_file_info: 0.000005,\n read_multiple_files: 0.00003,\n\n // ── Git operations ─────────────────────────────────────────────────────────\n git_status: 0.000005,\n git_diff: 0.00001,\n git_commit: 0.00005,\n git_push: 0.0002,\n git_pull: 0.0001,\n git_clone: 0.0005,\n git_log: 0.00001,\n git_branch: 0.000005,\n git_checkout: 0.00002,\n git_merge: 0.0001,\n\n // ── GitHub API ────────────────────────────────────────────────────────────\n create_issue: 0.0003,\n update_issue: 0.0003,\n close_issue: 0.0002,\n list_issues: 0.0001,\n get_issue: 0.0001,\n create_pull_request: 0.0003,\n merge_pull_request: 0.0005,\n list_pull_requests: 0.0001,\n search_code: 0.001,\n search_repositories: 0.0005,\n get_file_contents: 0.0001,\n create_or_update_file: 0.0003,\n fork_repository: 0.0005,\n create_repository: 0.0005,\n push_files: 0.0003,\n \"github_*\": 0.0002, // wildcard fallback for any github_ prefixed tool\n\n // ── Communication — Slack ─────────────────────────────────────────────────\n slack_post_message: 0.0001,\n slack_reply_to_thread: 0.0001,\n slack_add_reaction: 0.00005,\n slack_get_channel_info: 0.00005,\n slack_list_channels: 0.00008,\n slack_get_users: 0.0001,\n slack_upload_file: 0.0002,\n \"slack_*\": 0.00008,\n\n // ── Communication — email / other ─────────────────────────────────────────\n send_email: 0.001,\n send_slack_message: 0.0001,\n\n // ── Databases ─────────────────────────────────────────────────────────────\n postgres_query: 0.0005,\n postgres_execute: 0.001,\n postgres_insert: 0.001,\n postgres_update: 0.001,\n postgres_delete: 0.001,\n \"postgres_*\": 0.0005,\n mysql_query: 0.0005,\n \"mysql_*\": 0.0005,\n mongodb_find: 0.0002,\n mongodb_insert: 0.0005,\n \"mongodb_*\": 0.0003,\n redis_get: 0.00005,\n redis_set: 0.0001,\n \"redis_*\": 0.00008,\n sqlite_query: 0.00005,\n\n // ── HTTP / REST ───────────────────────────────────────────────────────────\n http_get: 0.0002,\n http_post: 0.0003,\n http_put: 0.0003,\n http_delete: 0.0002,\n http_fetch: 0.0002,\n fetch_url: 0.0002,\n make_api_request: 0.0003,\n \"http_*\": 0.0002,\n\n // ── Browser / web automation ──────────────────────────────────────────────\n browser_navigate: 0.002,\n browser_screenshot: 0.003,\n browser_click: 0.001,\n browser_type: 0.001,\n browser_scroll: 0.0005,\n browser_get_text: 0.001,\n browser_fill_form: 0.002,\n browser_wait: 0.0005,\n puppeteer_navigate: 0.002,\n playwright_goto: 0.002,\n \"browser_*\": 0.002,\n \"puppeteer_*\": 0.002,\n \"playwright_*\": 0.002,\n\n // ── Project management — Jira ─────────────────────────────────────────────\n create_jira_issue: 0.0003,\n update_jira_issue: 0.0003,\n search_jira_issues: 0.0002,\n get_jira_issue: 0.0001,\n add_jira_comment: 0.0002,\n \"jira_*\": 0.0002,\n\n // ── Project management — Linear / Notion / Asana ─────────────────────────\n create_linear_issue: 0.0003,\n \"linear_*\": 0.0002,\n notion_create_page: 0.0003,\n notion_update_page: 0.0003,\n notion_search: 0.0002,\n \"notion_*\": 0.0002,\n create_asana_task: 0.0003,\n \"asana_*\": 0.0002,\n\n // ── Vector DBs ────────────────────────────────────────────────────────────\n pinecone_query: 0.000001,\n pinecone_upsert: 0.000002,\n weaviate_query: 0.0000008,\n qdrant_search: 0.0000005,\n chroma_query: 0.0000005,\n\n // ── AWS ───────────────────────────────────────────────────────────────────\n lambda_invoke: 0.0000002,\n s3_get_object: 0.0000004,\n s3_put_object: 0.000005,\n s3_list_objects: 0.000005,\n dynamodb_get_item: 0.00000025,\n dynamodb_put_item: 0.00000125,\n dynamodb_query: 0.00000125,\n sqs_send_message: 0.00000040,\n sns_publish: 0.00000050,\n \"aws_*\": 0.000001,\n\n // ── Search / web ──────────────────────────────────────────────────────────\n brave_search: 0.000003,\n serper_search: 0.000001,\n tavily_search: 0.000002,\n exa_search: 0.000005,\n google_search: 0.000005,\n bing_search: 0.000003,\n duckduckgo_search: 0.000001,\n web_search: 0.000002,\n\n // ── Code execution ────────────────────────────────────────────────────────\n e2b_run_code: 0.000014,\n code_interpreter: 0.000014,\n bash_execute: 0.001,\n run_command: 0.001,\n run_terminal_command: 0.001,\n docker_run: 0.005,\n docker_exec: 0.002,\n\n // ── AI / embeddings ───────────────────────────────────────────────────────\n generate_embedding: 0.00001,\n openai_embedding: 0.00001,\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 /** @internal */\n _downstreamResource: string | 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 * Set the downstream resource identifier for this call.\n * Use this when the resource name is resolved inside the handler (e.g. after\n * looking up which Pinecone index to query).\n *\n * Convention:\n * - Pinecone: \"pinecone:<index-name>\" e.g. \"pinecone:product-embeddings\"\n * - Qdrant: \"qdrant:<collection>\" e.g. \"qdrant:support-docs\"\n * - Generic: just the provider name e.g. \"weaviate\", \"redis\"\n *\n * Overrides the `downstreamResource` option passed to wrapToolCall().\n *\n * @example\n * await prismMcp.wrapToolCall(\"vector_search\", async (ctx) => {\n * const index = await resolveIndex(query);\n * ctx.setDownstreamResource(`pinecone:${index}`);\n * return pinecone.query({ index, vector });\n * });\n */\n setDownstreamResource(resource: string): void {\n this._downstreamResource = resource;\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: ctx._downstreamResource ?? 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"]}
package/dist/index.mjs CHANGED
@@ -96,27 +96,150 @@ var SessionBudgetChecker = class {
96
96
 
97
97
  // src/pricing.ts
98
98
  var BUILT_IN = {
99
- // Vector DBs
99
+ // ── Filesystem & local I/O ─────────────────────────────────────────────────
100
+ read_file: 1e-5,
101
+ write_file: 5e-5,
102
+ create_file: 5e-5,
103
+ delete_file: 2e-5,
104
+ list_directory: 1e-5,
105
+ move_file: 2e-5,
106
+ copy_file: 2e-5,
107
+ search_files: 3e-5,
108
+ get_file_info: 5e-6,
109
+ read_multiple_files: 3e-5,
110
+ // ── Git operations ─────────────────────────────────────────────────────────
111
+ git_status: 5e-6,
112
+ git_diff: 1e-5,
113
+ git_commit: 5e-5,
114
+ git_push: 2e-4,
115
+ git_pull: 1e-4,
116
+ git_clone: 5e-4,
117
+ git_log: 1e-5,
118
+ git_branch: 5e-6,
119
+ git_checkout: 2e-5,
120
+ git_merge: 1e-4,
121
+ // ── GitHub API ────────────────────────────────────────────────────────────
122
+ create_issue: 3e-4,
123
+ update_issue: 3e-4,
124
+ close_issue: 2e-4,
125
+ list_issues: 1e-4,
126
+ get_issue: 1e-4,
127
+ create_pull_request: 3e-4,
128
+ merge_pull_request: 5e-4,
129
+ list_pull_requests: 1e-4,
130
+ search_code: 1e-3,
131
+ search_repositories: 5e-4,
132
+ get_file_contents: 1e-4,
133
+ create_or_update_file: 3e-4,
134
+ fork_repository: 5e-4,
135
+ create_repository: 5e-4,
136
+ push_files: 3e-4,
137
+ "github_*": 2e-4,
138
+ // wildcard fallback for any github_ prefixed tool
139
+ // ── Communication — Slack ─────────────────────────────────────────────────
140
+ slack_post_message: 1e-4,
141
+ slack_reply_to_thread: 1e-4,
142
+ slack_add_reaction: 5e-5,
143
+ slack_get_channel_info: 5e-5,
144
+ slack_list_channels: 8e-5,
145
+ slack_get_users: 1e-4,
146
+ slack_upload_file: 2e-4,
147
+ "slack_*": 8e-5,
148
+ // ── Communication — email / other ─────────────────────────────────────────
149
+ send_email: 1e-3,
150
+ send_slack_message: 1e-4,
151
+ // ── Databases ─────────────────────────────────────────────────────────────
152
+ postgres_query: 5e-4,
153
+ postgres_execute: 1e-3,
154
+ postgres_insert: 1e-3,
155
+ postgres_update: 1e-3,
156
+ postgres_delete: 1e-3,
157
+ "postgres_*": 5e-4,
158
+ mysql_query: 5e-4,
159
+ "mysql_*": 5e-4,
160
+ mongodb_find: 2e-4,
161
+ mongodb_insert: 5e-4,
162
+ "mongodb_*": 3e-4,
163
+ redis_get: 5e-5,
164
+ redis_set: 1e-4,
165
+ "redis_*": 8e-5,
166
+ sqlite_query: 5e-5,
167
+ // ── HTTP / REST ───────────────────────────────────────────────────────────
168
+ http_get: 2e-4,
169
+ http_post: 3e-4,
170
+ http_put: 3e-4,
171
+ http_delete: 2e-4,
172
+ http_fetch: 2e-4,
173
+ fetch_url: 2e-4,
174
+ make_api_request: 3e-4,
175
+ "http_*": 2e-4,
176
+ // ── Browser / web automation ──────────────────────────────────────────────
177
+ browser_navigate: 2e-3,
178
+ browser_screenshot: 3e-3,
179
+ browser_click: 1e-3,
180
+ browser_type: 1e-3,
181
+ browser_scroll: 5e-4,
182
+ browser_get_text: 1e-3,
183
+ browser_fill_form: 2e-3,
184
+ browser_wait: 5e-4,
185
+ puppeteer_navigate: 2e-3,
186
+ playwright_goto: 2e-3,
187
+ "browser_*": 2e-3,
188
+ "puppeteer_*": 2e-3,
189
+ "playwright_*": 2e-3,
190
+ // ── Project management — Jira ─────────────────────────────────────────────
191
+ create_jira_issue: 3e-4,
192
+ update_jira_issue: 3e-4,
193
+ search_jira_issues: 2e-4,
194
+ get_jira_issue: 1e-4,
195
+ add_jira_comment: 2e-4,
196
+ "jira_*": 2e-4,
197
+ // ── Project management — Linear / Notion / Asana ─────────────────────────
198
+ create_linear_issue: 3e-4,
199
+ "linear_*": 2e-4,
200
+ notion_create_page: 3e-4,
201
+ notion_update_page: 3e-4,
202
+ notion_search: 2e-4,
203
+ "notion_*": 2e-4,
204
+ create_asana_task: 3e-4,
205
+ "asana_*": 2e-4,
206
+ // ── Vector DBs ────────────────────────────────────────────────────────────
100
207
  pinecone_query: 1e-6,
101
208
  pinecone_upsert: 2e-6,
102
209
  weaviate_query: 8e-7,
103
210
  qdrant_search: 5e-7,
104
- // AWS
211
+ chroma_query: 5e-7,
212
+ // ── AWS ───────────────────────────────────────────────────────────────────
105
213
  lambda_invoke: 2e-7,
106
214
  s3_get_object: 4e-7,
107
215
  s3_put_object: 5e-6,
216
+ s3_list_objects: 5e-6,
108
217
  dynamodb_get_item: 25e-8,
109
218
  dynamodb_put_item: 125e-8,
110
- // Search / web
219
+ dynamodb_query: 125e-8,
220
+ sqs_send_message: 4e-7,
221
+ sns_publish: 5e-7,
222
+ "aws_*": 1e-6,
223
+ // ── Search / web ──────────────────────────────────────────────────────────
111
224
  brave_search: 3e-6,
112
225
  serper_search: 1e-6,
113
226
  tavily_search: 2e-6,
114
227
  exa_search: 5e-6,
115
- // Code execution
228
+ google_search: 5e-6,
229
+ bing_search: 3e-6,
230
+ duckduckgo_search: 1e-6,
231
+ web_search: 2e-6,
232
+ // ── Code execution ────────────────────────────────────────────────────────
116
233
  e2b_run_code: 14e-6,
117
234
  code_interpreter: 14e-6,
118
- // Communication
119
- send_email: 1e-6
235
+ bash_execute: 1e-3,
236
+ run_command: 1e-3,
237
+ run_terminal_command: 1e-3,
238
+ docker_run: 5e-3,
239
+ docker_exec: 2e-3,
240
+ // ── AI / embeddings ───────────────────────────────────────────────────────
241
+ generate_embedding: 1e-5,
242
+ openai_embedding: 1e-5
120
243
  };
121
244
  function lookupToolCost(toolName, overrides = {}) {
122
245
  const all = { ...BUILT_IN, ...overrides };
@@ -170,6 +293,8 @@ var WrapContext = class {
170
293
  constructor() {
171
294
  /** @internal - read by _wrap() after fn() completes */
172
295
  this._actualCostUsd = null;
296
+ /** @internal */
297
+ this._downstreamResource = null;
173
298
  }
174
299
  /**
175
300
  * Override the catalog-estimated tool cost with the real billing figure.
@@ -187,6 +312,28 @@ var WrapContext = class {
187
312
  reportActualCost(usd) {
188
313
  this._actualCostUsd = usd;
189
314
  }
315
+ /**
316
+ * Set the downstream resource identifier for this call.
317
+ * Use this when the resource name is resolved inside the handler (e.g. after
318
+ * looking up which Pinecone index to query).
319
+ *
320
+ * Convention:
321
+ * - Pinecone: "pinecone:<index-name>" e.g. "pinecone:product-embeddings"
322
+ * - Qdrant: "qdrant:<collection>" e.g. "qdrant:support-docs"
323
+ * - Generic: just the provider name e.g. "weaviate", "redis"
324
+ *
325
+ * Overrides the `downstreamResource` option passed to wrapToolCall().
326
+ *
327
+ * @example
328
+ * await prismMcp.wrapToolCall("vector_search", async (ctx) => {
329
+ * const index = await resolveIndex(query);
330
+ * ctx.setDownstreamResource(`pinecone:${index}`);
331
+ * return pinecone.query({ index, vector });
332
+ * });
333
+ */
334
+ setDownstreamResource(resource) {
335
+ this._downstreamResource = resource;
336
+ }
190
337
  };
191
338
  var PrismMCP = class {
192
339
  constructor(options = {}) {
@@ -241,7 +388,7 @@ var PrismMCP = class {
241
388
  user_id: "",
242
389
  environment: this.opts.environment,
243
390
  tool_name: name,
244
- downstream_resource: extra.downstreamResource ?? "",
391
+ downstream_resource: ctx._downstreamResource ?? extra.downstreamResource ?? "",
245
392
  execution_latency_ms: latencyMs,
246
393
  tool_cost_usd: actualCostUsd ?? estimatedCost,
247
394
  cost_status: actualCostUsd != null ? "actual" : "estimated",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tracker.ts","../src/types.ts","../src/budget.ts","../src/pricing.ts","../src/prism-mcp.ts","../src/session.ts"],"sourcesContent":["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":";AAEA,SAAS,mBAA2B;AAClC,QAAM,UACJ,QAAQ,IAAI,eAAe,KAC3B,QAAQ,IAAI,qBAAqB,KACjC,wBACA,QAAQ,OAAO,EAAE;AACnB,SAAO,GAAG,MAAM;AAClB;AAEA,SAAS,WAAW,KAAqB;AACvC,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,SAAO,MAAM,UAAU,IAAK,MAAM,CAAC,KAAK,KAAM;AAChD;AAEO,IAAM,kBAAN,MAAsB;AAAA,EAM3B,YAAY,KAAa,YAAoB,WAAoB;AAC/D,SAAK,MAAa;AAClB,SAAK,YAAa,aAAa,iBAAiB;AAChD,SAAK,aAAa;AAClB,SAAK,QAAa,WAAW,GAAG;AAAA,EAClC;AAAA,EAEA,MAAM,QAAQ,OAAiF;AAC7F,QAAI;AACF,YAAM,OAAiB;AAAA,QACrB,GAAG;AAAA,QACH,UAAiB,OAAO,WAAW;AAAA,QACnC,QAAiB,KAAK;AAAA,QACtB,iBAAiB,KAAK;AAAA,MACxB;AAEA,YAAM,MAAM,MAAM,MAAM,KAAK,WAAW;AAAA,QACtC,QAAS;AAAA,QACT,SAAS;AAAA,UACP,eAAgB,UAAU,KAAK,GAAG;AAAA,UAClC,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;AAAA,MACzC,CAAC;AAED,UAAI,CAAC,IAAI,MAAM,IAAI,WAAW,KAAK;AAEjC,gBAAQ,KAAK,+BAA+B,IAAI,MAAM,EAAE;AAAA,MAC1D;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;ACgBO,IAAM,kCAAN,cAA8C,MAAM;AAAA,EACzD,YAAY,WAAmB,WAAmB;AAChD;AAAA,MACE,kCAAkC,SAAS,yBAAyB,SAAS;AAAA,IAE/E;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACjD,YAAY,WAAmB,OAAe;AAC5C;AAAA,MACE,kCAAkC,KAAK,wBAAwB,SAAS;AAAA,IAE1E;AACA,SAAK,OAAO;AAAA,EACd;AACF;;;AC9EA,eAAe,SAAS,KAAa,OAAe,KAA8B;AAChF,QAAM,MAAM,MAAM,MAAM,GAAG,GAAG,QAAQ,mBAAmB,GAAG,CAAC,IAAI;AAAA,IAC/D,SAAS,EAAE,eAAe,UAAU,KAAK,GAAG;AAAA,EAC9C,CAAC;AACD,MAAI,CAAC,IAAI,GAAI,QAAO;AACpB,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,SAAO,WAAW,KAAK,UAAU,GAAG,KAAK;AAC3C;AAEO,IAAM,uBAAN,MAA2B;AAAA,EAKhC,YAAY,OAAe;AACzB,SAAK,MAAQ,QAAQ,IAAI,wBAAwB,KAAO;AACxD,SAAK,QAAQ,QAAQ,IAAI,0BAA0B,KAAK;AACxD,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,aACJ,WACA,kBACA,wBACe;AACf,QAAI,CAAC,KAAK,OAAO,CAAC,KAAK,MAAO;AAE9B,QAAI,oBAAoB,QAAQ,mBAAmB,GAAG;AACpD,YAAM,UAAU,WAAW,KAAK,KAAK,IAAI,SAAS;AAClD,YAAM,UAAU,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,OAAO;AAC5D,UAAI,WAAW,kBAAkB;AAC/B,cAAM,IAAI,gCAAgC,WAAW,gBAAgB;AAAA,MACvE;AAAA,IACF;AAEA,QAAI,0BAA0B,QAAQ,yBAAyB,GAAG;AAChE,YAAM,UAAU,WAAW,KAAK,KAAK,IAAI,SAAS;AAClD,YAAM,QAAU,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,OAAO;AAC5D,UAAI,SAAS,wBAAwB;AACnC,cAAM,IAAI,wBAAwB,WAAW,sBAAsB;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AACF;;;ACrDA,IAAM,WAAmC;AAAA;AAAA,EAEvC,gBAAwB;AAAA,EACxB,iBAAwB;AAAA,EACxB,gBAAwB;AAAA,EACxB,eAAwB;AAAA;AAAA,EAExB,eAAwB;AAAA,EACxB,eAAwB;AAAA,EACxB,eAAwB;AAAA,EACxB,mBAAwB;AAAA,EACxB,mBAAwB;AAAA;AAAA,EAExB,cAAwB;AAAA,EACxB,eAAwB;AAAA,EACxB,eAAwB;AAAA,EACxB,YAAwB;AAAA;AAAA,EAExB,cAAwB;AAAA,EACxB,kBAAwB;AAAA;AAAA,EAExB,YAAwB;AAC1B;AAMO,SAAS,eACd,UACA,YAAoC,CAAC,GAC7B;AACR,QAAM,MAAM,EAAE,GAAG,UAAU,GAAG,UAAU;AAGxC,MAAI,YAAY,IAAK,QAAO,IAAI,QAAQ;AAGxC,QAAM,SAAS,OAAO,KAAK,GAAG,EAC3B,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,SAAS,WAAW,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC,EACpE,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AAExC,SAAO,SAAU,IAAI,MAAM,KAAK,IAAK;AACvC;;;AC3BA,SAASA,YAAW,KAAqB;AACvC,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,SAAO,MAAM,UAAU,IAAK,MAAM,CAAC,KAAK,KAAM;AAChD;AAEA,IAAM,sBAAsB,CAAC,YAAY,SAAS,OAAO,UAAU,WAAW,eAAe;AAE7F,SAAS,aACP,KACA,YACS;AACT,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO;AACpD,MAAI,MAAM,QAAQ,GAAG,EAAG,QAAO,IAAI,IAAI,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AACzE,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAA8B,GAAG;AACnE,QAAI,CAAC,IAAI,WAAW,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,IACrE,eACA,aAAa,GAAG,UAAU;AAAA,EAChC;AACA,SAAO;AACT;AAEA,SAAS,SAAS,GAAW,KAAqB;AAChD,SAAO,EAAE,UAAU,MAAM,IAAI,EAAE,MAAM,GAAG,GAAG,IAAI;AACjD;AAEA,SAAS,SAAS,KAAc,YAAsB,QAAwB;AAC5E,MAAI;AACF,WAAO,SAAS,KAAK,UAAU,aAAa,KAAK,UAAU,CAAC,GAAG,MAAM;AAAA,EACvE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,SAAS,gBAAgB,KAA6C;AACpE,SAAO,OAAO,QACZ,OAAQ,IAA+B,OAAO,aAAa,MAAM;AACrE;AAMA,gBAAgB,mBACd,QACA,OACA,OACmB;AACnB,MAAI,QAAQ;AACZ,MAAI;AACF,qBAAiB,SAAS,QAAQ;AAChC,YAAM;AAAA,IACR;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ;AACR,UAAM;AAAA,EACR,UAAE;AACA,UAAM,KAAK,IAAI,IAAI,OAAO,KAAK;AAAA,EACjC;AACF;AAIO,IAAM,cAAN,MAAkB;AAAA,EAAlB;AAEL;AAAA,0BAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAehC,iBAAiB,KAAmB;AAClC,SAAK,iBAAiB;AAAA,EACxB;AACF;AAIO,IAAM,WAAN,MAAe;AAAA,EASpB,YAAY,UAA2B,CAAC,GAAG;AACzC,UAAM,MAAM,QAAQ,YAAY,QAAQ,IAAI,eAAe,KAAK;AAChE,QAAI,CAAC,KAAK;AACR,cAAQ,KAAK,sEAAiE;AAAA,IAChF;AAEA,SAAK,OAAO;AAAA,MACV,SAAwB,QAAQ,WAAkB,QAAQ,IAAI,eAAe,KAAS;AAAA,MACtF,MAAwB,QAAQ,QAAkB,QAAQ,IAAI,YAAY,KAAY;AAAA,MACtF,aAAwB,QAAQ,eAAkB,QAAQ,IAAI,mBAAmB,KAAK;AAAA,MACtF,WAAwB,QAAQ,aAAkB,OAAO,WAAW;AAAA,MACpE,YAAwB,QAAQ,cAAkB;AAAA,MAClD,kBAAwB,QAAQ;AAAA,MAChC,wBAAwB,QAAQ;AAAA,MAChC,eAAwB,QAAQ,iBAAkB;AAAA,MAClD,gBAAwB,QAAQ,kBAAkB;AAAA,IACpD;AACA,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,UAAU,IAAI,gBAAgB,KAAK,KAAK,KAAK,YAAY,QAAQ,SAAS;AAC/E,SAAK,SAAU,IAAI,qBAAqBA,YAAW,GAAG,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,MACZ,eACA,MACA,IACA,QAKI,CAAC,GACO;AAEZ,UAAM,KAAK,OAAO;AAAA,MAChB,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,MACV,KAAK,KAAK;AAAA,IACZ;AAEA,UAAM,QAAW,KAAK,IAAI;AAC1B,QAAM,SAAuC;AAC7C,QAAM,WAAW;AACjB,QAAM;AACN,UAAM,MAAW,IAAI,YAAY;AAGjC,UAAM,YAAoC,EAAE,GAAG,MAAM,KAAK;AAC1D,QAAI,KAAK,KAAK,iBAAiB,MAAM,UAAU,MAAM;AACnD,gBAAU,YAAY,IAAI,SAAS,MAAM,QAAQ,KAAK,YAAY,GAAI;AAAA,IACxE;AAGA,UAAM,gBAAgB,kBAAkB,SAAS,eAAe,IAAI,IAAI;AAExE,UAAM,YAAY,CAAC,WAAmB,gBAA4C;AAChF,YAAM,gBAAgB,IAAI;AAC1B,WAAK,QAAQ,QAAQ;AAAA,QACnB,YAAsB,oBAAI,KAAK,GAAE,YAAY,EAAE,QAAQ,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE;AAAA,QAC5E,YAAsB,KAAK,KAAK;AAAA,QAChC,YAAsB,KAAK,KAAK;AAAA,QAChC,SAAsB,KAAK,KAAK;AAAA,QAChC,SAAsB;AAAA,QACtB,aAAsB,KAAK,KAAK;AAAA,QAChC,WAAsB;AAAA,QACtB,qBAAsB,MAAM,sBAAsB;AAAA,QAClD,sBAAsB;AAAA,QACtB,eAAsB,iBAAiB;AAAA,QACvC,aAAsB,iBAAiB,OAAO,WAAW;AAAA,QACzD,QAAsB;AAAA,QACtB,eAAsB;AAAA,QACtB,gBAAsB,MAAM,gBAAgB;AAAA,QAC5C,gBAAsB;AAAA,QACtB,MAAsB;AAAA,MACxB,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACnB;AAEA,QAAI;AACF,eAAS,MAAM,GAAG,GAAG;AAAA,IACvB,SAAS,KAAK;AACZ,eAAW;AACX,iBAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC1D,gBAAU,KAAK,IAAI,IAAI,OAAO,MAAM;AACpC,YAAM;AAAA,IACR;AAGA,QAAI,KAAK,KAAK,kBAAkB,UAAU,QAAQ,CAAC,gBAAgB,MAAM,GAAG;AAC1E,gBAAU,aAAa,IAAI,SAAS,QAAQ,KAAK,YAAY,GAAI;AAAA,IACnE;AAGA,QAAI,gBAAgB,MAAM,GAAG;AAC3B,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,CAAC,WAAW,UAAU;AACpB,oBAAU,WAAW,QAAQ,UAAU,IAAI;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAGA,cAAU,KAAK,IAAI,IAAI,OAAO,MAAM;AACpC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aACJ,UACA,IACA,QAMI,CAAC,GACO;AACZ,WAAO,KAAK,MAAM,QAAQ,UAAU,IAAI,KAAK;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aACE,SAC8B;AAC9B,UAAM,OAAO;AACb,WAAO,eAAe,eAAe,KAA0B;AAC7D,aAAO,KAAK;AAAA,QACV,IAAI,OAAO;AAAA,QACX,CAAC,QAAQ,QAAQ,KAAK,GAAG;AAAA,QACzB,EAAE,QAAQ,IAAI,OAAO,UAAU;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,iBACJ,aACA,IACA,QAGI,CAAC,GACO;AACZ,WAAO,KAAK,MAAM,YAAY,aAAa,IAAI,KAAK;AAAA,EACtD;AAAA,EAEA,qBACE,SAC8B;AAC9B,UAAM,OAAO;AACb,WAAO,eAAe,uBAAuB,KAA0B;AACrE,aAAO,KAAK,iBAAiB,IAAI,OAAO,KAAK,CAAC,QAAQ,QAAQ,KAAK,GAAG,CAAC;AAAA,IACzE;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,cACJ,YACA,IACA,QAGI,CAAC,GACO;AACZ,WAAO,KAAK,MAAM,UAAU,YAAY,IAAI,KAAK;AAAA,EACnD;AAAA,EAEA,mBACE,SAC8B;AAC9B,UAAM,OAAO;AACb,WAAO,eAAe,qBAAqB,KAA0B;AACnE,aAAO,KAAK,cAAc,IAAI,OAAO,MAAM,CAAC,QAAQ,QAAQ,KAAK,GAAG,CAAC;AAAA,IACvE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,oBAIE,SAC8B;AAC9B,UAAM,OAAO;AACb,WAAO,eAAe,uBAAuB,KAA0B;AAErE,YAAM,YACJ,IAAI,QAAQ,kBAAkB,QAAQ,CAAC,GAAG,QAAQ;AACpD,aAAO,KAAK,MAAM,YAAY,WAAW,CAAC,SAAS,QAAQ,GAAG,CAAC;AAAA,IACjE;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,YAAoB;AAAE,WAAO,KAAK,KAAK;AAAA,EAAW;AACxD;;;AC9RO,IAAM,eAAN,MAAmB;AAAA,EASxB,YAAY,UAA+B,CAAC,GAAG;AAC7C,SAAK,MAAc,QAAQ,YAAc,QAAQ,IAAI,eAAe,KAAS;AAC7E,SAAK,UAAc,QAAQ,WAAc,QAAQ,IAAI,eAAe,KAAS;AAC7E,SAAK,OAAc,QAAQ,QAAc,QAAQ,IAAI,YAAY,KAAY;AAC7E,SAAK,cAAc,QAAQ,eAAe,QAAQ,IAAI,mBAAmB,KAAK;AAC9E,SAAK,YAAc,QAAQ,aAAc,OAAO,WAAW;AAC3D,SAAK,aAAc;AAEnB,QAAI,CAAC,KAAK,KAAK;AACb,cAAQ,KAAK,gFAA2E;AAAA,IAC1F;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,YAA8B,CAAC,GAAa;AACvD,WAAO,IAAI,SAAS;AAAA,MAClB,UAAwB,KAAK;AAAA,MAC7B,SAAwB,KAAK;AAAA,MAC7B,MAAwB,KAAK;AAAA,MAC7B,aAAwB,KAAK;AAAA,MAC7B,WAAwB,KAAK;AAAA,MAC7B,kBAAwB,KAAK,WAAW;AAAA,MACxC,wBAAwB,KAAK,WAAW;AAAA,MACxC,eAAwB,KAAK,WAAW;AAAA,MACxC,gBAAwB,KAAK,WAAW;AAAA,MACxC,YAAwB,KAAK,WAAW;AAAA,MACxC,WAAwB,KAAK,WAAW;AAAA;AAAA,MAExC,YAAwB,UAAU;AAAA,IACpC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAME;AACA,WAAO;AAAA,MACL,UAAa,KAAK;AAAA,MAClB,SAAa,KAAK;AAAA,MAClB,MAAa,KAAK;AAAA,MAClB,aAAa,KAAK;AAAA,MAClB,WAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;","names":["orgFromKey"]}
1
+ {"version":3,"sources":["../src/tracker.ts","../src/types.ts","../src/budget.ts","../src/pricing.ts","../src/prism-mcp.ts","../src/session.ts"],"sourcesContent":["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 * Pricing basis:\n * - Filesystem / Git: trivial I/O; estimated at $0.01/1K ops\n * - GitHub API: free tier 5 000 req/hr; charged via Actions minutes at ~$0.0003/req\n * - Slack / email: negligible API cost; estimated at overhead cost\n * - Databases: typical query cost on managed DB (Neon/RDS) per operation\n * - HTTP / browser: outbound network + optional headless browser compute\n * - AWS: official published rates (Lambda $0.0000002/req, S3 $0.004/10K GET)\n * - Search APIs: published per-query pricing\n * - Code execution: E2B/code_interpreter published rates\n *\n * Prefix wildcards (\"github_*\") match any tool whose name starts with that prefix.\n * lookupToolCost() tries exact → longest-prefix → 0.\n */\n\nconst BUILT_IN: Record<string, number> = {\n // ── Filesystem & local I/O ─────────────────────────────────────────────────\n read_file: 0.00001,\n write_file: 0.00005,\n create_file: 0.00005,\n delete_file: 0.00002,\n list_directory: 0.00001,\n move_file: 0.00002,\n copy_file: 0.00002,\n search_files: 0.00003,\n get_file_info: 0.000005,\n read_multiple_files: 0.00003,\n\n // ── Git operations ─────────────────────────────────────────────────────────\n git_status: 0.000005,\n git_diff: 0.00001,\n git_commit: 0.00005,\n git_push: 0.0002,\n git_pull: 0.0001,\n git_clone: 0.0005,\n git_log: 0.00001,\n git_branch: 0.000005,\n git_checkout: 0.00002,\n git_merge: 0.0001,\n\n // ── GitHub API ────────────────────────────────────────────────────────────\n create_issue: 0.0003,\n update_issue: 0.0003,\n close_issue: 0.0002,\n list_issues: 0.0001,\n get_issue: 0.0001,\n create_pull_request: 0.0003,\n merge_pull_request: 0.0005,\n list_pull_requests: 0.0001,\n search_code: 0.001,\n search_repositories: 0.0005,\n get_file_contents: 0.0001,\n create_or_update_file: 0.0003,\n fork_repository: 0.0005,\n create_repository: 0.0005,\n push_files: 0.0003,\n \"github_*\": 0.0002, // wildcard fallback for any github_ prefixed tool\n\n // ── Communication — Slack ─────────────────────────────────────────────────\n slack_post_message: 0.0001,\n slack_reply_to_thread: 0.0001,\n slack_add_reaction: 0.00005,\n slack_get_channel_info: 0.00005,\n slack_list_channels: 0.00008,\n slack_get_users: 0.0001,\n slack_upload_file: 0.0002,\n \"slack_*\": 0.00008,\n\n // ── Communication — email / other ─────────────────────────────────────────\n send_email: 0.001,\n send_slack_message: 0.0001,\n\n // ── Databases ─────────────────────────────────────────────────────────────\n postgres_query: 0.0005,\n postgres_execute: 0.001,\n postgres_insert: 0.001,\n postgres_update: 0.001,\n postgres_delete: 0.001,\n \"postgres_*\": 0.0005,\n mysql_query: 0.0005,\n \"mysql_*\": 0.0005,\n mongodb_find: 0.0002,\n mongodb_insert: 0.0005,\n \"mongodb_*\": 0.0003,\n redis_get: 0.00005,\n redis_set: 0.0001,\n \"redis_*\": 0.00008,\n sqlite_query: 0.00005,\n\n // ── HTTP / REST ───────────────────────────────────────────────────────────\n http_get: 0.0002,\n http_post: 0.0003,\n http_put: 0.0003,\n http_delete: 0.0002,\n http_fetch: 0.0002,\n fetch_url: 0.0002,\n make_api_request: 0.0003,\n \"http_*\": 0.0002,\n\n // ── Browser / web automation ──────────────────────────────────────────────\n browser_navigate: 0.002,\n browser_screenshot: 0.003,\n browser_click: 0.001,\n browser_type: 0.001,\n browser_scroll: 0.0005,\n browser_get_text: 0.001,\n browser_fill_form: 0.002,\n browser_wait: 0.0005,\n puppeteer_navigate: 0.002,\n playwright_goto: 0.002,\n \"browser_*\": 0.002,\n \"puppeteer_*\": 0.002,\n \"playwright_*\": 0.002,\n\n // ── Project management — Jira ─────────────────────────────────────────────\n create_jira_issue: 0.0003,\n update_jira_issue: 0.0003,\n search_jira_issues: 0.0002,\n get_jira_issue: 0.0001,\n add_jira_comment: 0.0002,\n \"jira_*\": 0.0002,\n\n // ── Project management — Linear / Notion / Asana ─────────────────────────\n create_linear_issue: 0.0003,\n \"linear_*\": 0.0002,\n notion_create_page: 0.0003,\n notion_update_page: 0.0003,\n notion_search: 0.0002,\n \"notion_*\": 0.0002,\n create_asana_task: 0.0003,\n \"asana_*\": 0.0002,\n\n // ── Vector DBs ────────────────────────────────────────────────────────────\n pinecone_query: 0.000001,\n pinecone_upsert: 0.000002,\n weaviate_query: 0.0000008,\n qdrant_search: 0.0000005,\n chroma_query: 0.0000005,\n\n // ── AWS ───────────────────────────────────────────────────────────────────\n lambda_invoke: 0.0000002,\n s3_get_object: 0.0000004,\n s3_put_object: 0.000005,\n s3_list_objects: 0.000005,\n dynamodb_get_item: 0.00000025,\n dynamodb_put_item: 0.00000125,\n dynamodb_query: 0.00000125,\n sqs_send_message: 0.00000040,\n sns_publish: 0.00000050,\n \"aws_*\": 0.000001,\n\n // ── Search / web ──────────────────────────────────────────────────────────\n brave_search: 0.000003,\n serper_search: 0.000001,\n tavily_search: 0.000002,\n exa_search: 0.000005,\n google_search: 0.000005,\n bing_search: 0.000003,\n duckduckgo_search: 0.000001,\n web_search: 0.000002,\n\n // ── Code execution ────────────────────────────────────────────────────────\n e2b_run_code: 0.000014,\n code_interpreter: 0.000014,\n bash_execute: 0.001,\n run_command: 0.001,\n run_terminal_command: 0.001,\n docker_run: 0.005,\n docker_exec: 0.002,\n\n // ── AI / embeddings ───────────────────────────────────────────────────────\n generate_embedding: 0.00001,\n openai_embedding: 0.00001,\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 /** @internal */\n _downstreamResource: string | 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 * Set the downstream resource identifier for this call.\n * Use this when the resource name is resolved inside the handler (e.g. after\n * looking up which Pinecone index to query).\n *\n * Convention:\n * - Pinecone: \"pinecone:<index-name>\" e.g. \"pinecone:product-embeddings\"\n * - Qdrant: \"qdrant:<collection>\" e.g. \"qdrant:support-docs\"\n * - Generic: just the provider name e.g. \"weaviate\", \"redis\"\n *\n * Overrides the `downstreamResource` option passed to wrapToolCall().\n *\n * @example\n * await prismMcp.wrapToolCall(\"vector_search\", async (ctx) => {\n * const index = await resolveIndex(query);\n * ctx.setDownstreamResource(`pinecone:${index}`);\n * return pinecone.query({ index, vector });\n * });\n */\n setDownstreamResource(resource: string): void {\n this._downstreamResource = resource;\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: ctx._downstreamResource ?? 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prism-llm-labs/mcp-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP middleware for Prism LLM observability — intercepts tool calls, tracks costs, enforces session budgets",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/pricing.ts CHANGED
@@ -1,30 +1,179 @@
1
1
  /**
2
2
  * Built-in tool cost estimates (USD per call).
3
3
  * Mirrors apps/web/lib/pricing/tool-catalog.ts — keep in sync.
4
+ *
5
+ * Pricing basis:
6
+ * - Filesystem / Git: trivial I/O; estimated at $0.01/1K ops
7
+ * - GitHub API: free tier 5 000 req/hr; charged via Actions minutes at ~$0.0003/req
8
+ * - Slack / email: negligible API cost; estimated at overhead cost
9
+ * - Databases: typical query cost on managed DB (Neon/RDS) per operation
10
+ * - HTTP / browser: outbound network + optional headless browser compute
11
+ * - AWS: official published rates (Lambda $0.0000002/req, S3 $0.004/10K GET)
12
+ * - Search APIs: published per-query pricing
13
+ * - Code execution: E2B/code_interpreter published rates
14
+ *
15
+ * Prefix wildcards ("github_*") match any tool whose name starts with that prefix.
16
+ * lookupToolCost() tries exact → longest-prefix → 0.
4
17
  */
5
18
 
6
19
  const BUILT_IN: Record<string, number> = {
7
- // Vector DBs
8
- pinecone_query: 0.000001,
9
- pinecone_upsert: 0.000002,
10
- weaviate_query: 0.0000008,
11
- qdrant_search: 0.0000005,
12
- // AWS
13
- lambda_invoke: 0.0000002,
14
- s3_get_object: 0.0000004,
15
- s3_put_object: 0.000005,
16
- dynamodb_get_item: 0.00000025,
17
- dynamodb_put_item: 0.00000125,
18
- // Search / web
19
- brave_search: 0.000003,
20
- serper_search: 0.000001,
21
- tavily_search: 0.000002,
22
- exa_search: 0.000005,
23
- // Code execution
24
- e2b_run_code: 0.000014,
25
- code_interpreter: 0.000014,
26
- // Communication
27
- send_email: 0.000001,
20
+ // ── Filesystem & local I/O ─────────────────────────────────────────────────
21
+ read_file: 0.00001,
22
+ write_file: 0.00005,
23
+ create_file: 0.00005,
24
+ delete_file: 0.00002,
25
+ list_directory: 0.00001,
26
+ move_file: 0.00002,
27
+ copy_file: 0.00002,
28
+ search_files: 0.00003,
29
+ get_file_info: 0.000005,
30
+ read_multiple_files: 0.00003,
31
+
32
+ // ── Git operations ─────────────────────────────────────────────────────────
33
+ git_status: 0.000005,
34
+ git_diff: 0.00001,
35
+ git_commit: 0.00005,
36
+ git_push: 0.0002,
37
+ git_pull: 0.0001,
38
+ git_clone: 0.0005,
39
+ git_log: 0.00001,
40
+ git_branch: 0.000005,
41
+ git_checkout: 0.00002,
42
+ git_merge: 0.0001,
43
+
44
+ // ── GitHub API ────────────────────────────────────────────────────────────
45
+ create_issue: 0.0003,
46
+ update_issue: 0.0003,
47
+ close_issue: 0.0002,
48
+ list_issues: 0.0001,
49
+ get_issue: 0.0001,
50
+ create_pull_request: 0.0003,
51
+ merge_pull_request: 0.0005,
52
+ list_pull_requests: 0.0001,
53
+ search_code: 0.001,
54
+ search_repositories: 0.0005,
55
+ get_file_contents: 0.0001,
56
+ create_or_update_file: 0.0003,
57
+ fork_repository: 0.0005,
58
+ create_repository: 0.0005,
59
+ push_files: 0.0003,
60
+ "github_*": 0.0002, // wildcard fallback for any github_ prefixed tool
61
+
62
+ // ── Communication — Slack ─────────────────────────────────────────────────
63
+ slack_post_message: 0.0001,
64
+ slack_reply_to_thread: 0.0001,
65
+ slack_add_reaction: 0.00005,
66
+ slack_get_channel_info: 0.00005,
67
+ slack_list_channels: 0.00008,
68
+ slack_get_users: 0.0001,
69
+ slack_upload_file: 0.0002,
70
+ "slack_*": 0.00008,
71
+
72
+ // ── Communication — email / other ─────────────────────────────────────────
73
+ send_email: 0.001,
74
+ send_slack_message: 0.0001,
75
+
76
+ // ── Databases ─────────────────────────────────────────────────────────────
77
+ postgres_query: 0.0005,
78
+ postgres_execute: 0.001,
79
+ postgres_insert: 0.001,
80
+ postgres_update: 0.001,
81
+ postgres_delete: 0.001,
82
+ "postgres_*": 0.0005,
83
+ mysql_query: 0.0005,
84
+ "mysql_*": 0.0005,
85
+ mongodb_find: 0.0002,
86
+ mongodb_insert: 0.0005,
87
+ "mongodb_*": 0.0003,
88
+ redis_get: 0.00005,
89
+ redis_set: 0.0001,
90
+ "redis_*": 0.00008,
91
+ sqlite_query: 0.00005,
92
+
93
+ // ── HTTP / REST ───────────────────────────────────────────────────────────
94
+ http_get: 0.0002,
95
+ http_post: 0.0003,
96
+ http_put: 0.0003,
97
+ http_delete: 0.0002,
98
+ http_fetch: 0.0002,
99
+ fetch_url: 0.0002,
100
+ make_api_request: 0.0003,
101
+ "http_*": 0.0002,
102
+
103
+ // ── Browser / web automation ──────────────────────────────────────────────
104
+ browser_navigate: 0.002,
105
+ browser_screenshot: 0.003,
106
+ browser_click: 0.001,
107
+ browser_type: 0.001,
108
+ browser_scroll: 0.0005,
109
+ browser_get_text: 0.001,
110
+ browser_fill_form: 0.002,
111
+ browser_wait: 0.0005,
112
+ puppeteer_navigate: 0.002,
113
+ playwright_goto: 0.002,
114
+ "browser_*": 0.002,
115
+ "puppeteer_*": 0.002,
116
+ "playwright_*": 0.002,
117
+
118
+ // ── Project management — Jira ─────────────────────────────────────────────
119
+ create_jira_issue: 0.0003,
120
+ update_jira_issue: 0.0003,
121
+ search_jira_issues: 0.0002,
122
+ get_jira_issue: 0.0001,
123
+ add_jira_comment: 0.0002,
124
+ "jira_*": 0.0002,
125
+
126
+ // ── Project management — Linear / Notion / Asana ─────────────────────────
127
+ create_linear_issue: 0.0003,
128
+ "linear_*": 0.0002,
129
+ notion_create_page: 0.0003,
130
+ notion_update_page: 0.0003,
131
+ notion_search: 0.0002,
132
+ "notion_*": 0.0002,
133
+ create_asana_task: 0.0003,
134
+ "asana_*": 0.0002,
135
+
136
+ // ── Vector DBs ────────────────────────────────────────────────────────────
137
+ pinecone_query: 0.000001,
138
+ pinecone_upsert: 0.000002,
139
+ weaviate_query: 0.0000008,
140
+ qdrant_search: 0.0000005,
141
+ chroma_query: 0.0000005,
142
+
143
+ // ── AWS ───────────────────────────────────────────────────────────────────
144
+ lambda_invoke: 0.0000002,
145
+ s3_get_object: 0.0000004,
146
+ s3_put_object: 0.000005,
147
+ s3_list_objects: 0.000005,
148
+ dynamodb_get_item: 0.00000025,
149
+ dynamodb_put_item: 0.00000125,
150
+ dynamodb_query: 0.00000125,
151
+ sqs_send_message: 0.00000040,
152
+ sns_publish: 0.00000050,
153
+ "aws_*": 0.000001,
154
+
155
+ // ── Search / web ──────────────────────────────────────────────────────────
156
+ brave_search: 0.000003,
157
+ serper_search: 0.000001,
158
+ tavily_search: 0.000002,
159
+ exa_search: 0.000005,
160
+ google_search: 0.000005,
161
+ bing_search: 0.000003,
162
+ duckduckgo_search: 0.000001,
163
+ web_search: 0.000002,
164
+
165
+ // ── Code execution ────────────────────────────────────────────────────────
166
+ e2b_run_code: 0.000014,
167
+ code_interpreter: 0.000014,
168
+ bash_execute: 0.001,
169
+ run_command: 0.001,
170
+ run_terminal_command: 0.001,
171
+ docker_run: 0.005,
172
+ docker_exec: 0.002,
173
+
174
+ // ── AI / embeddings ───────────────────────────────────────────────────────
175
+ generate_embedding: 0.00001,
176
+ openai_embedding: 0.00001,
28
177
  };
29
178
 
30
179
  /**
package/src/prism-mcp.ts CHANGED
@@ -86,7 +86,9 @@ async function* proxyAsyncIterable<T>(
86
86
 
87
87
  export class WrapContext {
88
88
  /** @internal - read by _wrap() after fn() completes */
89
- _actualCostUsd: number | null = null;
89
+ _actualCostUsd: number | null = null;
90
+ /** @internal */
91
+ _downstreamResource: string | null = null;
90
92
 
91
93
  /**
92
94
  * Override the catalog-estimated tool cost with the real billing figure.
@@ -104,6 +106,29 @@ export class WrapContext {
104
106
  reportActualCost(usd: number): void {
105
107
  this._actualCostUsd = usd;
106
108
  }
109
+
110
+ /**
111
+ * Set the downstream resource identifier for this call.
112
+ * Use this when the resource name is resolved inside the handler (e.g. after
113
+ * looking up which Pinecone index to query).
114
+ *
115
+ * Convention:
116
+ * - Pinecone: "pinecone:<index-name>" e.g. "pinecone:product-embeddings"
117
+ * - Qdrant: "qdrant:<collection>" e.g. "qdrant:support-docs"
118
+ * - Generic: just the provider name e.g. "weaviate", "redis"
119
+ *
120
+ * Overrides the `downstreamResource` option passed to wrapToolCall().
121
+ *
122
+ * @example
123
+ * await prismMcp.wrapToolCall("vector_search", async (ctx) => {
124
+ * const index = await resolveIndex(query);
125
+ * ctx.setDownstreamResource(`pinecone:${index}`);
126
+ * return pinecone.query({ index, vector });
127
+ * });
128
+ */
129
+ setDownstreamResource(resource: string): void {
130
+ this._downstreamResource = resource;
131
+ }
107
132
  }
108
133
 
109
134
  // ── Main class ────────────────────────────────────────────────────────────────
@@ -189,7 +214,7 @@ export class PrismMCP {
189
214
  user_id: "",
190
215
  environment: this.opts.environment,
191
216
  tool_name: name,
192
- downstream_resource: extra.downstreamResource ?? "",
217
+ downstream_resource: ctx._downstreamResource ?? extra.downstreamResource ?? "",
193
218
  execution_latency_ms: latencyMs,
194
219
  tool_cost_usd: actualCostUsd ?? estimatedCost,
195
220
  cost_status: actualCostUsd != null ? "actual" : "estimated",