pluribus-context 0.3.37 → 0.3.39

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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  All notable changes to Pluribus are documented here.
6
6
 
7
+ ## 0.3.39 - 2026-06-07
8
+
9
+ - Added `pluribus demo mcp-telemetry-import`, a tiny npm-runnable converter from MCP `rpc-messages.jsonl`-style JSON-RPC traces into privacy-safe audit receipts that preserve attribution, redacted shapes, status, and timing gaps without storing raw tool payloads.
10
+
11
+ ## 0.3.38 - 2026-06-06
12
+
13
+ - Added `pluribus demo mcp-audit-receipt`, a tiny npm-runnable demo that validates privacy-safe MCP tool-call audit events and low-cardinality usage metrics without logging raw prompts, args, results, tokens, or row data.
14
+
7
15
  ## 0.3.37 - 2026-06-06
8
16
 
9
17
  - Published the canonical top-level Agent Skills layout (`skills/*/SKILL.md`) and backwards-compatible legacy mirrors so external skill registries can keep source links verifiable while package users get the current recipes from npm.
package/bin/pluribus.js CHANGED
@@ -67,7 +67,8 @@ OPTIONS (watch)
67
67
  --debounce Debounce delay in ms (minimum 300, default 400)
68
68
 
69
69
  OPTIONS (demo)
70
- --receipt Validate a custom skill use-rate receipt JSON file
70
+ --receipt Validate a custom demo receipt JSON file
71
+ --input Import a custom demo input file, such as rpc-messages.jsonl
71
72
  --json Print machine-readable demo results
72
73
 
73
74
  EXAMPLES
@@ -89,6 +90,10 @@ EXAMPLES
89
90
  pluribus watch --tools claude,cursor
90
91
  pluribus demo skill-use-rate
91
92
  pluribus demo skill-use-rate --json
93
+ pluribus demo mcp-audit-receipt
94
+ pluribus demo mcp-audit-receipt --json
95
+ pluribus demo mcp-telemetry-import
96
+ pluribus demo mcp-telemetry-import --json
92
97
 
93
98
  DOCS
94
99
  https://github.com/caioribeiroclw-pixel/pluribus
@@ -100,7 +105,7 @@ const COMMAND_FLAGS = {
100
105
  validate: new Set(['source', 'update-imports']),
101
106
  audit: new Set(['source', 'tools', 'update-imports', 'strict', 'ci', 'json', 'output', 'github-annotations', 'fidelity-report']),
102
107
  watch: new Set(['source', 'tools', 'update-imports', 'dry-run', 'once', 'debounce']),
103
- demo: new Set(['receipt', 'json']),
108
+ demo: new Set(['receipt', 'input', 'json']),
104
109
  }
105
110
 
106
111
  function getFlagNames(argv) {
@@ -0,0 +1,24 @@
1
+ # MCP audit receipt demo
2
+
3
+ This example validates a privacy-safe audit receipt for MCP `tools/call` activity.
4
+
5
+ Run from any directory after `pluribus-context@latest` is published:
6
+
7
+ ```bash
8
+ npx --yes pluribus-context@latest demo mcp-audit-receipt
9
+ npx --yes pluribus-context@latest demo mcp-audit-receipt --json
10
+ ```
11
+
12
+ Or validate your own receipt shape:
13
+
14
+ ```bash
15
+ npx --yes pluribus-context@latest demo mcp-audit-receipt --receipt ./mcp-audit-receipt.json
16
+ ```
17
+
18
+ The point is to split:
19
+
20
+ - **audit events**: correlation IDs, hashed user/token subject, token scopes, tool name, redacted argument shape, status, duration, result shape, and error class;
21
+ - **usage metrics**: low-cardinality counters/histograms such as tool name, status, and token scope;
22
+ - **privacy boundary**: no raw prompts, raw SQL, row data, tokens, tool outputs, or private connection strings in the receipt.
23
+
24
+ This is for MCP server/gateway operators who need to answer: “who invoked which tool, under what scope, and did it succeed?” without dumping sensitive content into logs.
@@ -0,0 +1,70 @@
1
+ {
2
+ "schema": "pluribus.mcp_tool_call_audit_receipt.v1",
3
+ "run_id": "mcp-audit-2026-06-06T22:00Z",
4
+ "generated_at": "2026-06-06T22:00:00Z",
5
+ "server": {
6
+ "name": "analytics-mcp",
7
+ "transport": "http",
8
+ "version": "1.4.0"
9
+ },
10
+ "client": {
11
+ "name": "claude-desktop",
12
+ "workspace": "hashed:7f3f0f5f"
13
+ },
14
+ "audit_policy": {
15
+ "raw_arguments": "redacted_shape_only",
16
+ "raw_results": "redacted_shape_only",
17
+ "privacy_boundary": "no prompts, raw SQL, row data, tokens, or private connection strings in receipt"
18
+ },
19
+ "tool_calls": [
20
+ {
21
+ "event": "mcp.tool_call",
22
+ "request_id": "req_01JY6GATEWAY",
23
+ "session_id": "sess_01JY6RUN",
24
+ "user_id_hash": "sha256:0d9b4a1a3f0a6f2fd0dc8aa9d0c6f2b7a35f4f5d0b2a4d3e1e04a8b4b6b2e5d8",
25
+ "token_subject_hash": "sha256:6aaf4a3b4d10c45c8fd2cb4f3c73b8cde42bb62779b7e1c6a2c5e0dd8d78f4a1",
26
+ "token_scopes": ["database:read", "query:run"],
27
+ "tool_name": "query_database",
28
+ "args_shape": {
29
+ "database_id": "number",
30
+ "query": "redacted_sql",
31
+ "limit": "number"
32
+ },
33
+ "status": "ok",
34
+ "duration_ms": 184,
35
+ "result_shape": "rows:12 columns:4",
36
+ "error_class": null
37
+ },
38
+ {
39
+ "event": "mcp.tool_call",
40
+ "request_id": "req_01JY6DENIED",
41
+ "session_id": "sess_01JY6RUN",
42
+ "user_id_hash": "sha256:0d9b4a1a3f0a6f2fd0dc8aa9d0c6f2b7a35f4f5d0b2a4d3e1e04a8b4b6b2e5d8",
43
+ "token_subject_hash": "sha256:6aaf4a3b4d10c45c8fd2cb4f3c73b8cde42bb62779b7e1c6a2c5e0dd8d78f4a1",
44
+ "token_scopes": ["database:read"],
45
+ "tool_name": "update_dashboard",
46
+ "args_shape": {
47
+ "dashboard_id": "number",
48
+ "mutation": "redacted_object"
49
+ },
50
+ "status": "denied",
51
+ "duration_ms": 12,
52
+ "result_shape": "policy_denial",
53
+ "error_class": "insufficient_scope"
54
+ }
55
+ ],
56
+ "usage_metrics": [
57
+ {
58
+ "name": "mcp_tool_calls_total",
59
+ "type": "counter",
60
+ "value": "1",
61
+ "labels": ["tool_name", "status", "token_scope"]
62
+ },
63
+ {
64
+ "name": "mcp_tool_call_duration_ms",
65
+ "type": "histogram",
66
+ "value": "184",
67
+ "labels": ["tool_name", "status"]
68
+ }
69
+ ]
70
+ }
@@ -0,0 +1,27 @@
1
+ # MCP telemetry import demo
2
+
3
+ This example converts a tiny MCP `rpc-messages.jsonl`-style trace into the same privacy-safe audit receipt shape used by `pluribus demo mcp-audit-receipt`.
4
+
5
+ Run from any directory after `pluribus-context@latest` includes this demo:
6
+
7
+ ```bash
8
+ npx --yes pluribus-context@latest demo mcp-telemetry-import
9
+ npx --yes pluribus-context@latest demo mcp-telemetry-import --json
10
+ ```
11
+
12
+ Or convert your own log:
13
+
14
+ ```bash
15
+ npx --yes pluribus-context@latest demo mcp-telemetry-import --input ./rpc-messages.jsonl --json
16
+ ```
17
+
18
+ The point is not to store raw MCP payloads forever. The import keeps only:
19
+
20
+ - request/session IDs;
21
+ - hashed user/token subjects;
22
+ - token scopes;
23
+ - tool name;
24
+ - redacted argument/result shape;
25
+ - status, duration if timestamps exist, and error class.
26
+
27
+ If only fallback `rpc-messages.jsonl` exists, the receipt can still prove tool-call attribution. If gateway telemetry is absent, latency/status coverage should be marked as a gap instead of silently implied.
@@ -0,0 +1,4 @@
1
+ {"timestamp":"2026-06-07T13:00:00.000Z","direction":"client_to_server","session_id":"sess_demo","user_id":"user-123","token_subject":"oauth-subject-456","token_scopes":["repo:read"],"message":{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"github.search_issues","arguments":{"query":"repo:org/app label:bug MCP audit","limit":5}}}}
2
+ {"timestamp":"2026-06-07T13:00:00.142Z","direction":"server_to_client","session_id":"sess_demo","message":{"jsonrpc":"2.0","id":"1","result":{"content":[{"type":"text","text":"2 issues found"}],"isError":false}}}
3
+ {"timestamp":"2026-06-07T13:00:02.000Z","direction":"client_to_server","session_id":"sess_demo","user_id":"user-123","token_subject":"oauth-subject-456","token_scopes":["repo:read"],"message":{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"github.create_issue","arguments":{"repo":"org/app","title":"Add audit log","body":"redacted before receipt export"}}}}
4
+ {"timestamp":"2026-06-07T13:00:02.019Z","direction":"server_to_client","session_id":"sess_demo","message":{"jsonrpc":"2.0","id":"2","error":{"code":"insufficient_scope","message":"write scope required"}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluribus-context",
3
- "version": "0.3.37",
3
+ "version": "0.3.39",
4
4
  "description": "AI context and rules sync CLI for Claude.md, Claude Code, Cursor, and Copilot instructions, with privacy-safe context receipts that prove what memory, tools, skills, compactions, and security findings crossed agent boundaries without logging raw content.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/caioribeiroclw-pixel/pluribus#readme",
@@ -4,10 +4,16 @@
4
4
 
5
5
  import * as fs from 'fs'
6
6
  import * as path from 'path'
7
+ import { createHash } from 'crypto'
7
8
  import { fileURLToPath } from 'url'
8
9
 
9
10
  const DEFAULT_DEMO = 'skill-use-rate'
11
+ const SKILL_USE_RATE_DEMO = 'skill-use-rate'
12
+ const MCP_AUDIT_RECEIPT_DEMO = 'mcp-audit-receipt'
13
+ const MCP_TELEMETRY_IMPORT_DEMO = 'mcp-telemetry-import'
14
+ const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO, MCP_TELEMETRY_IMPORT_DEMO]
10
15
  const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
16
+ const MCP_AUDIT_RECEIPT_SCHEMA = 'pluribus.mcp_tool_call_audit_receipt.v1'
11
17
 
12
18
  /**
13
19
  * @param {Record<string, string | boolean>} args
@@ -16,29 +22,44 @@ const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
16
22
  export async function runDemo(args, positional = []) {
17
23
  const demoName = positional[0] || DEFAULT_DEMO
18
24
 
19
- if (demoName !== DEFAULT_DEMO) {
20
- console.error(`❌ Unknown demo: ${demoName}`)
21
- console.error(' Available demos: skill-use-rate')
22
- process.exit(1)
25
+ switch (demoName) {
26
+ case SKILL_USE_RATE_DEMO:
27
+ return runSkillUseRateDemo(args)
28
+ case MCP_AUDIT_RECEIPT_DEMO:
29
+ return runMcpAuditReceiptDemo(args)
30
+ case MCP_TELEMETRY_IMPORT_DEMO:
31
+ return runMcpTelemetryImportDemo(args)
32
+ default:
33
+ console.error(`❌ Unknown demo: ${demoName}`)
34
+ console.error(` Available demos: ${AVAILABLE_DEMOS.join(', ')}`)
35
+ process.exit(1)
23
36
  }
37
+ }
24
38
 
25
- const receiptPath = typeof args.receipt === 'string' && args.receipt.trim()
26
- ? path.resolve(process.cwd(), args.receipt)
27
- : bundledSkillUseRateReceiptPath()
28
-
29
- let receipt
39
+ function readReceipt(receiptPath, label) {
30
40
  try {
31
- receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'))
41
+ return JSON.parse(fs.readFileSync(receiptPath, 'utf8'))
32
42
  } catch (err) {
33
- console.error(`❌ Could not read skill use-rate receipt at ${receiptPath}: ${err.message}`)
43
+ console.error(`❌ Could not read ${label} receipt at ${receiptPath}: ${err.message}`)
34
44
  process.exit(1)
35
45
  }
46
+ }
36
47
 
48
+ function selectedReceiptPath(args, defaultPath) {
49
+ return typeof args.receipt === 'string' && args.receipt.trim()
50
+ ? path.resolve(process.cwd(), args.receipt)
51
+ : defaultPath
52
+ }
53
+
54
+ function runSkillUseRateDemo(args) {
55
+ const receiptPath = selectedReceiptPath(args, bundledSkillUseRateReceiptPath())
56
+ const receipt = readReceipt(receiptPath, 'skill use-rate')
37
57
  const result = validateSkillUseRateReceipt(receipt)
58
+
38
59
  if (Boolean(args.json)) {
39
60
  console.log(JSON.stringify({
40
61
  ok: result.errors.length === 0,
41
- demo: DEFAULT_DEMO,
62
+ demo: SKILL_USE_RATE_DEMO,
42
63
  receipt: path.relative(process.cwd(), receiptPath) || receiptPath,
43
64
  summary: result.summary,
44
65
  warnings: result.warnings,
@@ -65,10 +86,108 @@ export async function runDemo(args, positional = []) {
65
86
  if (result.errors.length > 0) process.exit(1)
66
87
  }
67
88
 
89
+ function selectedInputPath(args, defaultPath) {
90
+ return typeof args.input === 'string' && args.input.trim()
91
+ ? path.resolve(process.cwd(), args.input)
92
+ : defaultPath
93
+ }
94
+
95
+ function runMcpAuditReceiptDemo(args) {
96
+ const receiptPath = selectedReceiptPath(args, bundledMcpAuditReceiptPath())
97
+ const receipt = readReceipt(receiptPath, 'MCP audit')
98
+ const result = validateMcpAuditReceipt(receipt)
99
+
100
+ if (Boolean(args.json)) {
101
+ console.log(JSON.stringify({
102
+ ok: result.errors.length === 0,
103
+ demo: MCP_AUDIT_RECEIPT_DEMO,
104
+ receipt: path.relative(process.cwd(), receiptPath) || receiptPath,
105
+ summary: result.summary,
106
+ warnings: result.warnings,
107
+ errors: result.errors,
108
+ }, null, 2))
109
+ } else {
110
+ console.log('🧪 Pluribus demo: MCP audit receipt')
111
+ console.log(` Receipt: ${path.relative(process.cwd(), receiptPath) || receiptPath}`)
112
+ console.log('')
113
+
114
+ if (result.errors.length === 0) {
115
+ console.log(`✅ MCP audit receipt ok: ${result.summary.toolCallCount} tool calls, ${result.summary.auditEventCount} audit events, ${result.summary.metricCount} metrics`)
116
+ for (const warning of result.warnings) console.log(` • ${warning}`)
117
+ console.log('')
118
+ console.log('Why this matters: production MCP needs audit events and low-cardinality metrics, not raw prompt/tool dumps. Prove who invoked which tool, under which scope, with redacted argument/result shape.')
119
+ console.log('Try your own receipt: pluribus demo mcp-audit-receipt --receipt path/to/mcp-audit-receipt.json')
120
+ } else {
121
+ console.error('❌ MCP audit receipt invalid:')
122
+ for (const error of result.errors) console.error(` • ${error}`)
123
+ }
124
+ }
125
+
126
+ if (result.errors.length > 0) process.exit(1)
127
+ }
128
+
129
+
130
+ function runMcpTelemetryImportDemo(args) {
131
+ const inputPath = selectedInputPath(args, bundledMcpTelemetryJsonlPath())
132
+ let logText
133
+ try {
134
+ logText = fs.readFileSync(inputPath, 'utf8')
135
+ } catch (err) {
136
+ console.error(`❌ Could not read MCP telemetry JSONL at ${inputPath}: ${err.message}`)
137
+ process.exit(1)
138
+ }
139
+
140
+ const imported = importMcpTelemetryJsonl(logText)
141
+ const result = validateMcpAuditReceipt(imported.receipt)
142
+ const warnings = [...imported.warnings, ...result.warnings]
143
+
144
+ if (Boolean(args.json)) {
145
+ console.log(JSON.stringify({
146
+ ok: result.errors.length === 0,
147
+ demo: MCP_TELEMETRY_IMPORT_DEMO,
148
+ input: path.relative(process.cwd(), inputPath) || inputPath,
149
+ summary: {
150
+ ...result.summary,
151
+ parsedEntryCount: imported.summary.parsedEntryCount,
152
+ matchedResponseCount: imported.summary.matchedResponseCount,
153
+ missingGatewayLatency: imported.summary.missingGatewayLatency,
154
+ },
155
+ receipt: imported.receipt,
156
+ warnings,
157
+ errors: result.errors,
158
+ }, null, 2))
159
+ } else {
160
+ console.log('🧪 Pluribus demo: MCP telemetry import')
161
+ console.log(` Input: ${path.relative(process.cwd(), inputPath) || inputPath}`)
162
+ console.log('')
163
+
164
+ if (result.errors.length === 0) {
165
+ console.log(`✅ MCP telemetry imported: ${imported.summary.parsedEntryCount} JSONL entries → ${result.summary.toolCallCount} audit receipt tool calls`)
166
+ if (warnings.length > 0) for (const warning of warnings) console.log(` • ${warning}`)
167
+ console.log('')
168
+ console.log('Why this matters: rpc-messages.jsonl is a useful fallback, but it usually proves tool-call attribution before it proves gateway latency. Convert raw JSON-RPC traces into privacy-safe receipts, then mark missing gateway evidence explicitly.')
169
+ console.log('Try your own log: pluribus demo mcp-telemetry-import --input path/to/rpc-messages.jsonl --json')
170
+ } else {
171
+ console.error('❌ MCP telemetry import produced an invalid receipt:')
172
+ for (const error of result.errors) console.error(` • ${error}`)
173
+ }
174
+ }
175
+
176
+ if (result.errors.length > 0) process.exit(1)
177
+ }
178
+
68
179
  function bundledSkillUseRateReceiptPath() {
69
180
  return fileURLToPath(new URL('../../examples/skill-use-rate-receipts/skill-use-rate-receipt.json', import.meta.url))
70
181
  }
71
182
 
183
+ function bundledMcpAuditReceiptPath() {
184
+ return fileURLToPath(new URL('../../examples/mcp-audit-receipts/mcp-audit-receipt.json', import.meta.url))
185
+ }
186
+
187
+ function bundledMcpTelemetryJsonlPath() {
188
+ return fileURLToPath(new URL('../../examples/mcp-telemetry-import/sample-rpc-messages.jsonl', import.meta.url))
189
+ }
190
+
72
191
  export function validateSkillUseRateReceipt(receipt) {
73
192
  const errors = []
74
193
  const warnings = []
@@ -153,3 +272,268 @@ export function validateSkillUseRateReceipt(receipt) {
153
272
  },
154
273
  }
155
274
  }
275
+
276
+
277
+ export function importMcpTelemetryJsonl(logText) {
278
+ const warnings = []
279
+ const entries = []
280
+ const pending = new Map()
281
+ const toolCalls = []
282
+ let matchedResponseCount = 0
283
+ let missingGatewayLatency = true
284
+
285
+ for (const [lineIndex, rawLine] of logText.split(/\r?\n/).entries()) {
286
+ const line = rawLine.trim()
287
+ if (!line) continue
288
+ try {
289
+ const entry = JSON.parse(line)
290
+ entries.push(entry)
291
+ const message = unwrapMcpMessage(entry)
292
+ const timestamp = entry.timestamp || entry.time || message.timestamp || null
293
+
294
+ if (isToolCallRequest(message)) {
295
+ pending.set(String(message.id), { entry, message, timestamp, lineIndex })
296
+ } else if (message.id != null && pending.has(String(message.id))) {
297
+ const request = pending.get(String(message.id))
298
+ pending.delete(String(message.id))
299
+ matchedResponseCount++
300
+ const durationMs = durationBetween(request.timestamp, timestamp)
301
+ if (durationMs > 0) missingGatewayLatency = false
302
+ toolCalls.push(toolCallFromRequestResponse(request, message, durationMs))
303
+ }
304
+ } catch (err) {
305
+ warnings.push(`line ${lineIndex + 1} was skipped: invalid JSON (${err.message})`)
306
+ }
307
+ }
308
+
309
+ for (const request of pending.values()) {
310
+ toolCalls.push(toolCallFromRequestResponse(request, null, 0))
311
+ }
312
+
313
+ if (toolCalls.length === 0) warnings.push('no tools/call request/response pairs were found')
314
+ if (missingGatewayLatency) warnings.push('gateway.jsonl-style latency/status evidence is missing; fallback rpc-messages.jsonl can still prove tool-call attribution')
315
+
316
+ const receipt = {
317
+ schema: MCP_AUDIT_RECEIPT_SCHEMA,
318
+ run_id: 'mcp-telemetry-import-demo',
319
+ generated_at: '2026-06-07T13:00:00Z',
320
+ server: {
321
+ name: 'mcp-gateway-or-fallback-log',
322
+ transport: 'jsonrpc-jsonl',
323
+ version: 'unknown',
324
+ },
325
+ client: {
326
+ name: 'unknown-mcp-client',
327
+ workspace: 'redacted',
328
+ },
329
+ audit_policy: {
330
+ raw_arguments: 'redacted_shape_only',
331
+ raw_results: 'redacted_shape_only',
332
+ privacy_boundary: 'source JSONL may contain raw protocol data; receipt keeps only shapes, hashes, status, and timing evidence',
333
+ },
334
+ telemetry_source: {
335
+ kind: missingGatewayLatency ? 'rpc-messages.jsonl-fallback' : 'gateway-or-timestamped-jsonl',
336
+ parsed_entries: entries.length,
337
+ matched_responses: matchedResponseCount,
338
+ },
339
+ tool_calls: toolCalls,
340
+ usage_metrics: buildMcpUsageMetrics(toolCalls),
341
+ }
342
+
343
+ return {
344
+ receipt,
345
+ warnings,
346
+ summary: {
347
+ parsedEntryCount: entries.length,
348
+ matchedResponseCount,
349
+ missingGatewayLatency,
350
+ },
351
+ }
352
+ }
353
+
354
+ function unwrapMcpMessage(entry) {
355
+ return entry.message || entry.msg || entry.rpc || entry.jsonrpc_message || entry
356
+ }
357
+
358
+ function isToolCallRequest(message) {
359
+ return message && message.id != null && ['tools/call', 'tools.call', 'mcp.tools.call'].includes(message.method)
360
+ }
361
+
362
+ function toolCallFromRequestResponse(request, response, durationMs) {
363
+ const params = request.message.params || {}
364
+ const toolName = params.name || params.tool_name || params.tool || 'unknown_tool'
365
+ const status = response == null ? 'empty' : response.error ? 'error' : 'ok'
366
+ const resultShape = response == null ? 'missing_response' : response.error ? `error:${response.error.code || 'unknown'}` : shapeLabel(response.result)
367
+ const userSource = request.entry.user_id || request.entry.actor || request.entry.principal || request.entry.session_id || 'unknown-user'
368
+ const tokenSource = request.entry.token_subject || request.entry.token_id || request.entry.principal || 'unknown-token'
369
+
370
+ return {
371
+ event: 'mcp.tool_call',
372
+ request_id: String(request.message.id),
373
+ session_id: String(request.entry.session_id || request.entry.run_id || 'unknown-session'),
374
+ user_id_hash: privacyHash(userSource),
375
+ token_subject_hash: privacyHash(tokenSource),
376
+ token_scopes: Array.isArray(request.entry.token_scopes) && request.entry.token_scopes.length > 0 ? request.entry.token_scopes : ['unknown'],
377
+ tool_name: String(toolName),
378
+ args_shape: shapeObject(params.arguments || params.args || {}),
379
+ status,
380
+ duration_ms: Math.max(0, durationMs),
381
+ result_shape: resultShape,
382
+ error_class: response?.error ? String(response.error.code || response.error.message || 'mcp_error') : null,
383
+ }
384
+ }
385
+
386
+ function buildMcpUsageMetrics(toolCalls) {
387
+ const callsByStatus = new Map()
388
+ for (const call of toolCalls) {
389
+ const key = `${call.tool_name}:${call.status}:${call.token_scopes[0] || 'unknown'}`
390
+ callsByStatus.set(key, (callsByStatus.get(key) || 0) + 1)
391
+ }
392
+ const metrics = [...callsByStatus.entries()].map(([key, value]) => ({
393
+ name: 'mcp_tool_calls_total',
394
+ type: 'counter',
395
+ value: String(value),
396
+ labels: ['tool_name', 'status', 'token_scope'],
397
+ dimensions: key,
398
+ }))
399
+ const durations = toolCalls.filter((call) => call.duration_ms > 0)
400
+ if (durations.length > 0) {
401
+ metrics.push({
402
+ name: 'mcp_tool_call_duration_ms',
403
+ type: 'histogram',
404
+ value: String(Math.round(durations.reduce((sum, call) => sum + call.duration_ms, 0) / durations.length)),
405
+ labels: ['tool_name', 'status'],
406
+ })
407
+ }
408
+ return metrics.length > 0 ? metrics : [{ name: 'mcp_tool_calls_total', type: 'counter', value: '0', labels: ['tool_name', 'status'] }]
409
+ }
410
+
411
+ function durationBetween(start, end) {
412
+ if (!start || !end) return 0
413
+ const started = Date.parse(start)
414
+ const ended = Date.parse(end)
415
+ if (Number.isNaN(started) || Number.isNaN(ended) || ended < started) return 0
416
+ return ended - started
417
+ }
418
+
419
+ function privacyHash(value) {
420
+ return `sha256:${createHash('sha256').update(String(value)).digest('hex')}`
421
+ }
422
+
423
+ function shapeObject(value) {
424
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
425
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, shapeLabel(nested)]))
426
+ }
427
+
428
+ function shapeLabel(value) {
429
+ if (value === null) return 'null'
430
+ if (Array.isArray(value)) return `array:${value.length}`
431
+ if (typeof value === 'object') return `object:${Object.keys(value).length}`
432
+ if (typeof value === 'string') return looksSensitive(value) ? 'redacted_string' : 'string'
433
+ if (typeof value === 'number') return 'number'
434
+ if (typeof value === 'boolean') return 'boolean'
435
+ return typeof value
436
+ }
437
+
438
+ function looksSensitive(value) {
439
+ return /select\s|insert\s|update\s|delete\s|token|secret|password|bearer/i.test(value)
440
+ }
441
+
442
+ export function validateMcpAuditReceipt(receipt) {
443
+ const errors = []
444
+ const warnings = []
445
+
446
+ function requireString(value, field) {
447
+ if (typeof value !== 'string' || value.trim() === '') {
448
+ errors.push(`${field} must be a non-empty string`)
449
+ }
450
+ }
451
+
452
+ function requireArray(value, field) {
453
+ if (!Array.isArray(value) || value.length === 0) {
454
+ errors.push(`${field} must be a non-empty array`)
455
+ }
456
+ }
457
+
458
+ function requireNonNegativeNumber(value, field) {
459
+ if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
460
+ errors.push(`${field} must be a non-negative number`)
461
+ }
462
+ }
463
+
464
+ if (receipt.schema !== MCP_AUDIT_RECEIPT_SCHEMA) {
465
+ errors.push(`schema must be ${MCP_AUDIT_RECEIPT_SCHEMA}`)
466
+ }
467
+
468
+ requireString(receipt.run_id, 'run_id')
469
+ requireString(receipt.generated_at, 'generated_at')
470
+ requireString(receipt.server?.name, 'server.name')
471
+ requireString(receipt.server?.transport, 'server.transport')
472
+ requireString(receipt.client?.name, 'client.name')
473
+ requireString(receipt.audit_policy?.raw_arguments, 'audit_policy.raw_arguments')
474
+ requireString(receipt.audit_policy?.raw_results, 'audit_policy.raw_results')
475
+ requireString(receipt.audit_policy?.privacy_boundary, 'audit_policy.privacy_boundary')
476
+ requireArray(receipt.tool_calls, 'tool_calls')
477
+ requireArray(receipt.usage_metrics, 'usage_metrics')
478
+
479
+ if (receipt.audit_policy?.raw_arguments !== 'redacted_shape_only') {
480
+ errors.push('audit_policy.raw_arguments must be redacted_shape_only')
481
+ }
482
+ if (receipt.audit_policy?.raw_results !== 'redacted_shape_only') {
483
+ errors.push('audit_policy.raw_results must be redacted_shape_only')
484
+ }
485
+
486
+ const lowCardinalityMetricLabels = new Set(['tool_name', 'status', 'token_scope', 'user_type'])
487
+
488
+ for (const [index, call] of (receipt.tool_calls || []).entries()) {
489
+ const prefix = `tool_calls[${index}]`
490
+ requireString(call.event, `${prefix}.event`)
491
+ requireString(call.request_id, `${prefix}.request_id`)
492
+ requireString(call.session_id, `${prefix}.session_id`)
493
+ requireString(call.user_id_hash, `${prefix}.user_id_hash`)
494
+ requireString(call.token_subject_hash, `${prefix}.token_subject_hash`)
495
+ requireArray(call.token_scopes, `${prefix}.token_scopes`)
496
+ requireString(call.tool_name, `${prefix}.tool_name`)
497
+ requireString(call.status, `${prefix}.status`)
498
+ requireNonNegativeNumber(call.duration_ms, `${prefix}.duration_ms`)
499
+ requireString(call.result_shape, `${prefix}.result_shape`)
500
+
501
+ if (call.event !== 'mcp.tool_call') errors.push(`${prefix}.event must be mcp.tool_call`)
502
+ if (!['ok', 'empty', 'error', 'timeout', 'denied'].includes(call.status)) {
503
+ errors.push(`${prefix}.status must be one of ok|empty|error|timeout|denied`)
504
+ }
505
+ if (!call.args_shape || typeof call.args_shape !== 'object' || Array.isArray(call.args_shape)) {
506
+ errors.push(`${prefix}.args_shape must be an object with redacted argument types/shapes`)
507
+ }
508
+ if (typeof call.args_preview === 'string' || typeof call.result_preview === 'string') {
509
+ errors.push(`${prefix} must not include raw args/results previews; use args_shape/result_shape instead`)
510
+ }
511
+ if (call.error_class != null && typeof call.error_class !== 'string') {
512
+ errors.push(`${prefix}.error_class must be string or null`)
513
+ }
514
+ }
515
+
516
+ for (const [index, metric] of (receipt.usage_metrics || []).entries()) {
517
+ const prefix = `usage_metrics[${index}]`
518
+ requireString(metric.name, `${prefix}.name`)
519
+ requireString(metric.type, `${prefix}.type`)
520
+ requireString(metric.value, `${prefix}.value`)
521
+ requireArray(metric.labels, `${prefix}.labels`)
522
+
523
+ for (const label of metric.labels || []) {
524
+ if (!lowCardinalityMetricLabels.has(label)) {
525
+ warnings.push(`${prefix}.labels includes high-cardinality label ${label}; prefer ${[...lowCardinalityMetricLabels].join(', ')}`)
526
+ }
527
+ }
528
+ }
529
+
530
+ return {
531
+ errors,
532
+ warnings,
533
+ summary: {
534
+ toolCallCount: Array.isArray(receipt.tool_calls) ? receipt.tool_calls.length : 0,
535
+ auditEventCount: Array.isArray(receipt.tool_calls) ? receipt.tool_calls.length : 0,
536
+ metricCount: Array.isArray(receipt.usage_metrics) ? receipt.usage_metrics.length : 0,
537
+ },
538
+ }
539
+ }
@@ -1 +1 @@
1
- export const VERSION = '0.3.37'
1
+ export const VERSION = '0.3.39'