pluribus-context 0.3.38 → 0.3.40

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,15 @@
4
4
 
5
5
  All notable changes to Pluribus are documented here.
6
6
 
7
+ ## 0.3.40 - 2026-06-09
8
+
9
+ - Added `pluribus demo tool-surface-diff`, a tiny npm-runnable MCP dynamic-discovery receipt demo for proving discovered, activated, withheld, and blocked runtime tool-surface changes without logging raw schemas, prompts, or results.
10
+ - Expanded npm discovery keywords around MCP audit, gateways, security, tool discovery, and audit trails so the package is easier to find from the market lane now forming around MCP governance.
11
+
12
+ ## 0.3.39 - 2026-06-07
13
+
14
+ - 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.
15
+
7
16
  ## 0.3.38 - 2026-06-06
8
17
 
9
18
  - 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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ The original sync workflow is still useful: Pluribus can keep project instructio
14
14
 
15
15
  It is **not** a persistent memory layer, retrieval system, agent orchestrator, enterprise ContextOps platform, or agent-merging framework. Think evidence for context boundaries: `CLAUDE.md`, `.cursorrules`, `copilot-instructions.md`, `AGENTS.md`, MCP Tool Search, Agent Skills, RAG/code-search, pruning, and compaction — with privacy-safe receipts instead of raw content dumps.
16
16
 
17
- **Reviewer shortcut:** evaluating Pluribus for a list, newsletter, package roundup, or tool directory? Use the [Community Review Packet](docs/community-review-packet.md) for copy-paste directory submission fields, safety/removability notes, feedback links, and disposable 60-second smoke tests. If you only run one command for the cross-tool audit, try `npx --yes pluribus-context@latest audit --json --fidelity-report` to see native discovery surfaces, generic fallbacks, load evidence, duplicate-load selection evidence, manual activation requirements, effective context scope, and semantic differences. For the agent-observability wedge, start with [context-budget receipts](docs/context-budget-receipts.md): privacy-safe evidence for what MCP schemas, skills, memory, subagents, CLI help, retrieval chunks, pruning runs, or compaction summaries crossed an agent boundary. If you want the same idea as a copyable skill, use the [context-receipts Agent Skill recipe](skills/context-receipts/). npm `latest` is currently aligned with the GitHub release; the review packet also documents a GitHub-release smoke fallback for future release-lag windows.
17
+ **Reviewer shortcut:** evaluating Pluribus for a list, newsletter, package roundup, or tool directory? Use the [Community Review Packet](docs/community-review-packet.md) for copy-paste directory submission fields, safety/removability notes, feedback links, and disposable 60-second smoke tests. If you only run one command for the cross-tool audit, try `npx --yes pluribus-context@latest audit --json --fidelity-report` to see native discovery surfaces, generic fallbacks, load evidence, duplicate-load selection evidence, manual activation requirements, effective context scope, and semantic differences. For the agent-observability wedge, start with [context-budget receipts](docs/context-budget-receipts.md): privacy-safe evidence for what MCP schemas, skills, memory, subagents, CLI help, retrieval chunks, pruning runs, or compaction summaries crossed an agent boundary. It now explicitly covers the "Tool Search fixed MCP bloat" objection: the receipt proves which lane stayed deferred, which tool was expanded, and whether schemas leaked through `messages`/bootstrap anyway. For a 60-second runtime-discovery proof, run `npx --yes pluribus-context@latest demo tool-surface-diff --json`; it validates a receipt for discovered → activated → withheld/blocked MCP tools without raw schemas/prompts/results. If you want the same idea as a copyable skill, use the [context-receipts Agent Skill recipe](skills/context-receipts/). npm `latest` is currently aligned with the GitHub release; the review packet also documents a GitHub-release smoke fallback for future release-lag windows.
18
18
 
19
19
  ---
20
20
 
package/bin/pluribus.js CHANGED
@@ -68,6 +68,7 @@ OPTIONS (watch)
68
68
 
69
69
  OPTIONS (demo)
70
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
@@ -91,6 +92,10 @@ EXAMPLES
91
92
  pluribus demo skill-use-rate --json
92
93
  pluribus demo mcp-audit-receipt
93
94
  pluribus demo mcp-audit-receipt --json
95
+ pluribus demo mcp-telemetry-import
96
+ pluribus demo mcp-telemetry-import --json
97
+ pluribus demo tool-surface-diff
98
+ pluribus demo tool-surface-diff --json
94
99
 
95
100
  DOCS
96
101
  https://github.com/caioribeiroclw-pixel/pluribus
@@ -102,7 +107,7 @@ const COMMAND_FLAGS = {
102
107
  validate: new Set(['source', 'update-imports']),
103
108
  audit: new Set(['source', 'tools', 'update-imports', 'strict', 'ci', 'json', 'output', 'github-annotations', 'fidelity-report']),
104
109
  watch: new Set(['source', 'tools', 'update-imports', 'dry-run', 'once', 'debounce']),
105
- demo: new Set(['receipt', 'json']),
110
+ demo: new Set(['receipt', 'input', 'json']),
106
111
  }
107
112
 
108
113
  function getFlagNames(argv) {
@@ -8,6 +8,49 @@ This is different from generic token accounting. A context-budget receipt should
8
8
 
9
9
  If you want a copyable Agent Skill recipe instead of a spec-style guide, see [`skills/context-receipts/`](../skills/context-receipts/). It turns the receipt pattern into a 60-second smoke checklist for Tool Search, skills, and subagent boundaries.
10
10
 
11
+ ## If Tool Search already fixed the bloat
12
+
13
+ Modern hosts can defer large MCP catalogs behind Tool Search or similar lazy discovery. That changes the receipt question; it does not remove it.
14
+
15
+ Do not use a context-budget receipt to re-prove that every schema was smaller than before. Use it to prove the boundary that lazy loading promised:
16
+
17
+ - the catalog/index was loaded instead of full definitions;
18
+ - the selected query loaded only the matching tool definitions;
19
+ - unselected tool groups stayed deferred or withheld;
20
+ - a schema did not enter a side lane such as `messages`, subagent bootstrap, skill preamble, or memory hydration; and
21
+ - the receipt records what evidence is missing when only fallback client telemetry exists.
22
+
23
+ This makes the receipt useful in the common objection case: "MCP context bloat is solved by Tool Search." A good receipt should answer: **solved where, for this turn, through which lane, and with what proof?**
24
+
25
+ Runnable fixture for the normal happy path:
26
+
27
+ ```bash
28
+ node examples/context-input-evidence/convert-mcp-tool-search-log.mjs
29
+ ```
30
+
31
+ Public trace:
32
+
33
+ - `examples/context-input-evidence/mcp-tool-search-otel-trace.json`
34
+
35
+ Minimum hidden-bypass fields for managed seats or gateways:
36
+
37
+ ```json
38
+ {
39
+ "event.name": "mcp.deferral.evaluated",
40
+ "mcp.defer_loading.enabled": true,
41
+ "mcp.catalog.deferred": true,
42
+ "mcp.tool_search.selected_tool_count": 0,
43
+ "context.messages.remote_mcp_schema_count_bucket": "over_25",
44
+ "context.messages.delta_token_bucket": "under_100k",
45
+ "context.attribution": "remote_mcp_schema_in_messages",
46
+ "expected_behavior": "deferred_until_tool_search_match",
47
+ "verdict": "deferral_bypassed",
48
+ "privacy.raw_schema_included": false
49
+ }
50
+ ```
51
+
52
+ That shape deliberately avoids raw schema bodies, connector names, private URLs, and prompt text. It proves attribution, not whether the selected tool was semantically optimal.
53
+
11
54
  ## When to use this receipt
12
55
 
13
56
  Use a context-budget receipt when a coding agent looks lazy, fails with `prompt is too long`, or returns a tiny summary after a subagent/tool-heavy step and you need to distinguish:
@@ -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"}}}
@@ -0,0 +1,61 @@
1
+ {
2
+ "schema": "pluribus.mcp_tool_surface_diff_receipt.v1",
3
+ "run_id": "tool-surface-diff-demo",
4
+ "generated_at": "2026-06-09T13:00:00Z",
5
+ "platform": {
6
+ "name": "enterprise-mcp-dynamic-discovery",
7
+ "audit_sink": "admin-center-or-siem"
8
+ },
9
+ "catalog": {
10
+ "server_id": "mcp://sales-ops-gateway",
11
+ "previous_hash": "sha256:previous-catalog-redacted",
12
+ "current_hash": "sha256:current-catalog-redacted"
13
+ },
14
+ "runtime_discovery": {
15
+ "enabled": true,
16
+ "trigger": "runtime_tool_catalog_diff"
17
+ },
18
+ "privacy_boundary": {
19
+ "raw_schemas": "omitted_hash_only",
20
+ "raw_prompts": "omitted",
21
+ "raw_results": "omitted"
22
+ },
23
+ "tools": [
24
+ {
25
+ "tool_id": "tool:crm.search_accounts",
26
+ "name_hash": "sha256:0cc2efb4a26f4c5eb4f7d8c99e78d37adbdba07d50ee7873452c0216d02b1f48",
27
+ "schema_hash": "sha256:6f4fbe0a8be41b6e29c0c1c113aac38dfefdd12b89c6e9d4a996df2537acdb71",
28
+ "status": "activated",
29
+ "validation_outcome": "accepted",
30
+ "diff_summary": {
31
+ "added_fields": 1,
32
+ "removed_fields": 0,
33
+ "changed_fields": 0
34
+ }
35
+ },
36
+ {
37
+ "tool_id": "tool:crm.export_contacts",
38
+ "name_hash": "sha256:f38f53f9ba3c348e67332a24a7d15f5e7ab1c9253cf01f3451e15d9e15435e13",
39
+ "schema_hash": "sha256:95ddc9b86a0c2d8bc6f3ca0bcce93dca2469ced51b0d38c8f8c5aa88554e0032",
40
+ "status": "blocked",
41
+ "validation_outcome": "blocked_by_rai",
42
+ "diff_summary": {
43
+ "added_fields": 4,
44
+ "removed_fields": 0,
45
+ "changed_fields": 2
46
+ }
47
+ },
48
+ {
49
+ "tool_id": "tool:billing.refund_invoice",
50
+ "name_hash": "sha256:abfdaf6f0a3f33342aca6d8b0d63c303e99978481d26827843a901cb6237617d",
51
+ "schema_hash": "sha256:3b4fdc3ec15d2fa1cc589c1c7de280a19772d5745b4ef54d27806714be12bb8c",
52
+ "status": "withheld",
53
+ "validation_outcome": "entitlement_filtered",
54
+ "diff_summary": {
55
+ "added_fields": 0,
56
+ "removed_fields": 0,
57
+ "changed_fields": 0
58
+ }
59
+ }
60
+ ]
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluribus-context",
3
- "version": "0.3.38",
3
+ "version": "0.3.40",
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",
@@ -68,6 +68,11 @@
68
68
  "ai-agent-observability",
69
69
  "opentelemetry",
70
70
  "mcp",
71
+ "mcp-audit",
72
+ "mcp-gateway",
73
+ "mcp-security",
74
+ "tool-discovery",
75
+ "audit-trail",
71
76
  "drift-detection",
72
77
  "openclaw",
73
78
  "rules",
@@ -4,14 +4,18 @@
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'
10
11
  const SKILL_USE_RATE_DEMO = 'skill-use-rate'
11
12
  const MCP_AUDIT_RECEIPT_DEMO = 'mcp-audit-receipt'
12
- const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO]
13
+ const MCP_TELEMETRY_IMPORT_DEMO = 'mcp-telemetry-import'
14
+ const TOOL_SURFACE_DIFF_DEMO = 'tool-surface-diff'
15
+ const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO, MCP_TELEMETRY_IMPORT_DEMO, TOOL_SURFACE_DIFF_DEMO]
13
16
  const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
14
17
  const MCP_AUDIT_RECEIPT_SCHEMA = 'pluribus.mcp_tool_call_audit_receipt.v1'
18
+ const TOOL_SURFACE_DIFF_SCHEMA = 'pluribus.mcp_tool_surface_diff_receipt.v1'
15
19
 
16
20
  /**
17
21
  * @param {Record<string, string | boolean>} args
@@ -25,6 +29,10 @@ export async function runDemo(args, positional = []) {
25
29
  return runSkillUseRateDemo(args)
26
30
  case MCP_AUDIT_RECEIPT_DEMO:
27
31
  return runMcpAuditReceiptDemo(args)
32
+ case MCP_TELEMETRY_IMPORT_DEMO:
33
+ return runMcpTelemetryImportDemo(args)
34
+ case TOOL_SURFACE_DIFF_DEMO:
35
+ return runToolSurfaceDiffDemo(args)
28
36
  default:
29
37
  console.error(`❌ Unknown demo: ${demoName}`)
30
38
  console.error(` Available demos: ${AVAILABLE_DEMOS.join(', ')}`)
@@ -82,6 +90,12 @@ function runSkillUseRateDemo(args) {
82
90
  if (result.errors.length > 0) process.exit(1)
83
91
  }
84
92
 
93
+ function selectedInputPath(args, defaultPath) {
94
+ return typeof args.input === 'string' && args.input.trim()
95
+ ? path.resolve(process.cwd(), args.input)
96
+ : defaultPath
97
+ }
98
+
85
99
  function runMcpAuditReceiptDemo(args) {
86
100
  const receiptPath = selectedReceiptPath(args, bundledMcpAuditReceiptPath())
87
101
  const receipt = readReceipt(receiptPath, 'MCP audit')
@@ -116,6 +130,56 @@ function runMcpAuditReceiptDemo(args) {
116
130
  if (result.errors.length > 0) process.exit(1)
117
131
  }
118
132
 
133
+
134
+ function runMcpTelemetryImportDemo(args) {
135
+ const inputPath = selectedInputPath(args, bundledMcpTelemetryJsonlPath())
136
+ let logText
137
+ try {
138
+ logText = fs.readFileSync(inputPath, 'utf8')
139
+ } catch (err) {
140
+ console.error(`❌ Could not read MCP telemetry JSONL at ${inputPath}: ${err.message}`)
141
+ process.exit(1)
142
+ }
143
+
144
+ const imported = importMcpTelemetryJsonl(logText)
145
+ const result = validateMcpAuditReceipt(imported.receipt)
146
+ const warnings = [...imported.warnings, ...result.warnings]
147
+
148
+ if (Boolean(args.json)) {
149
+ console.log(JSON.stringify({
150
+ ok: result.errors.length === 0,
151
+ demo: MCP_TELEMETRY_IMPORT_DEMO,
152
+ input: path.relative(process.cwd(), inputPath) || inputPath,
153
+ summary: {
154
+ ...result.summary,
155
+ parsedEntryCount: imported.summary.parsedEntryCount,
156
+ matchedResponseCount: imported.summary.matchedResponseCount,
157
+ missingGatewayLatency: imported.summary.missingGatewayLatency,
158
+ },
159
+ receipt: imported.receipt,
160
+ warnings,
161
+ errors: result.errors,
162
+ }, null, 2))
163
+ } else {
164
+ console.log('🧪 Pluribus demo: MCP telemetry import')
165
+ console.log(` Input: ${path.relative(process.cwd(), inputPath) || inputPath}`)
166
+ console.log('')
167
+
168
+ if (result.errors.length === 0) {
169
+ console.log(`✅ MCP telemetry imported: ${imported.summary.parsedEntryCount} JSONL entries → ${result.summary.toolCallCount} audit receipt tool calls`)
170
+ if (warnings.length > 0) for (const warning of warnings) console.log(` • ${warning}`)
171
+ console.log('')
172
+ 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.')
173
+ console.log('Try your own log: pluribus demo mcp-telemetry-import --input path/to/rpc-messages.jsonl --json')
174
+ } else {
175
+ console.error('❌ MCP telemetry import produced an invalid receipt:')
176
+ for (const error of result.errors) console.error(` • ${error}`)
177
+ }
178
+ }
179
+
180
+ if (result.errors.length > 0) process.exit(1)
181
+ }
182
+
119
183
  function bundledSkillUseRateReceiptPath() {
120
184
  return fileURLToPath(new URL('../../examples/skill-use-rate-receipts/skill-use-rate-receipt.json', import.meta.url))
121
185
  }
@@ -124,6 +188,48 @@ function bundledMcpAuditReceiptPath() {
124
188
  return fileURLToPath(new URL('../../examples/mcp-audit-receipts/mcp-audit-receipt.json', import.meta.url))
125
189
  }
126
190
 
191
+ function bundledMcpTelemetryJsonlPath() {
192
+ return fileURLToPath(new URL('../../examples/mcp-telemetry-import/sample-rpc-messages.jsonl', import.meta.url))
193
+ }
194
+
195
+ function bundledToolSurfaceDiffReceiptPath() {
196
+ return fileURLToPath(new URL('../../examples/tool-surface-diff-receipts/tool-surface-diff-receipt.json', import.meta.url))
197
+ }
198
+
199
+ function runToolSurfaceDiffDemo(args) {
200
+ const receiptPath = selectedReceiptPath(args, bundledToolSurfaceDiffReceiptPath())
201
+ const receipt = readReceipt(receiptPath, 'tool-surface diff')
202
+ const result = validateToolSurfaceDiffReceipt(receipt)
203
+
204
+ if (Boolean(args.json)) {
205
+ console.log(JSON.stringify({
206
+ ok: result.errors.length === 0,
207
+ demo: TOOL_SURFACE_DIFF_DEMO,
208
+ receipt: path.relative(process.cwd(), receiptPath) || receiptPath,
209
+ summary: result.summary,
210
+ warnings: result.warnings,
211
+ errors: result.errors,
212
+ }, null, 2))
213
+ } else {
214
+ console.log('🧪 Pluribus demo: MCP tool-surface diff receipt')
215
+ console.log(` Receipt: ${path.relative(process.cwd(), receiptPath) || receiptPath}`)
216
+ console.log('')
217
+
218
+ if (result.errors.length === 0) {
219
+ console.log(`✅ tool-surface diff receipt ok: ${result.summary.discoveredCount} discovered, ${result.summary.activatedCount} activated, ${result.summary.withheldCount} withheld/blocked`)
220
+ for (const warning of result.warnings) console.log(` • ${warning}`)
221
+ console.log('')
222
+ console.log('Why this matters: runtime MCP discovery changes the active tool surface. Persist a low-cardinality receipt of discovered → activated → withheld/blocked tools without logging raw schemas, prompts, or results.')
223
+ console.log('Try your own receipt: pluribus demo tool-surface-diff --receipt path/to/tool-surface-diff-receipt.json --json')
224
+ } else {
225
+ console.error('❌ tool-surface diff receipt invalid:')
226
+ for (const error of result.errors) console.error(` • ${error}`)
227
+ }
228
+ }
229
+
230
+ if (result.errors.length > 0) process.exit(1)
231
+ }
232
+
127
233
  export function validateSkillUseRateReceipt(receipt) {
128
234
  const errors = []
129
235
  const warnings = []
@@ -209,6 +315,172 @@ export function validateSkillUseRateReceipt(receipt) {
209
315
  }
210
316
  }
211
317
 
318
+
319
+ export function importMcpTelemetryJsonl(logText) {
320
+ const warnings = []
321
+ const entries = []
322
+ const pending = new Map()
323
+ const toolCalls = []
324
+ let matchedResponseCount = 0
325
+ let missingGatewayLatency = true
326
+
327
+ for (const [lineIndex, rawLine] of logText.split(/\r?\n/).entries()) {
328
+ const line = rawLine.trim()
329
+ if (!line) continue
330
+ try {
331
+ const entry = JSON.parse(line)
332
+ entries.push(entry)
333
+ const message = unwrapMcpMessage(entry)
334
+ const timestamp = entry.timestamp || entry.time || message.timestamp || null
335
+
336
+ if (isToolCallRequest(message)) {
337
+ pending.set(String(message.id), { entry, message, timestamp, lineIndex })
338
+ } else if (message.id != null && pending.has(String(message.id))) {
339
+ const request = pending.get(String(message.id))
340
+ pending.delete(String(message.id))
341
+ matchedResponseCount++
342
+ const durationMs = durationBetween(request.timestamp, timestamp)
343
+ if (durationMs > 0) missingGatewayLatency = false
344
+ toolCalls.push(toolCallFromRequestResponse(request, message, durationMs))
345
+ }
346
+ } catch (err) {
347
+ warnings.push(`line ${lineIndex + 1} was skipped: invalid JSON (${err.message})`)
348
+ }
349
+ }
350
+
351
+ for (const request of pending.values()) {
352
+ toolCalls.push(toolCallFromRequestResponse(request, null, 0))
353
+ }
354
+
355
+ if (toolCalls.length === 0) warnings.push('no tools/call request/response pairs were found')
356
+ if (missingGatewayLatency) warnings.push('gateway.jsonl-style latency/status evidence is missing; fallback rpc-messages.jsonl can still prove tool-call attribution')
357
+
358
+ const receipt = {
359
+ schema: MCP_AUDIT_RECEIPT_SCHEMA,
360
+ run_id: 'mcp-telemetry-import-demo',
361
+ generated_at: '2026-06-07T13:00:00Z',
362
+ server: {
363
+ name: 'mcp-gateway-or-fallback-log',
364
+ transport: 'jsonrpc-jsonl',
365
+ version: 'unknown',
366
+ },
367
+ client: {
368
+ name: 'unknown-mcp-client',
369
+ workspace: 'redacted',
370
+ },
371
+ audit_policy: {
372
+ raw_arguments: 'redacted_shape_only',
373
+ raw_results: 'redacted_shape_only',
374
+ privacy_boundary: 'source JSONL may contain raw protocol data; receipt keeps only shapes, hashes, status, and timing evidence',
375
+ },
376
+ telemetry_source: {
377
+ kind: missingGatewayLatency ? 'rpc-messages.jsonl-fallback' : 'gateway-or-timestamped-jsonl',
378
+ parsed_entries: entries.length,
379
+ matched_responses: matchedResponseCount,
380
+ },
381
+ tool_calls: toolCalls,
382
+ usage_metrics: buildMcpUsageMetrics(toolCalls),
383
+ }
384
+
385
+ return {
386
+ receipt,
387
+ warnings,
388
+ summary: {
389
+ parsedEntryCount: entries.length,
390
+ matchedResponseCount,
391
+ missingGatewayLatency,
392
+ },
393
+ }
394
+ }
395
+
396
+ function unwrapMcpMessage(entry) {
397
+ return entry.message || entry.msg || entry.rpc || entry.jsonrpc_message || entry
398
+ }
399
+
400
+ function isToolCallRequest(message) {
401
+ return message && message.id != null && ['tools/call', 'tools.call', 'mcp.tools.call'].includes(message.method)
402
+ }
403
+
404
+ function toolCallFromRequestResponse(request, response, durationMs) {
405
+ const params = request.message.params || {}
406
+ const toolName = params.name || params.tool_name || params.tool || 'unknown_tool'
407
+ const status = response == null ? 'empty' : response.error ? 'error' : 'ok'
408
+ const resultShape = response == null ? 'missing_response' : response.error ? `error:${response.error.code || 'unknown'}` : shapeLabel(response.result)
409
+ const userSource = request.entry.user_id || request.entry.actor || request.entry.principal || request.entry.session_id || 'unknown-user'
410
+ const tokenSource = request.entry.token_subject || request.entry.token_id || request.entry.principal || 'unknown-token'
411
+
412
+ return {
413
+ event: 'mcp.tool_call',
414
+ request_id: String(request.message.id),
415
+ session_id: String(request.entry.session_id || request.entry.run_id || 'unknown-session'),
416
+ user_id_hash: privacyHash(userSource),
417
+ token_subject_hash: privacyHash(tokenSource),
418
+ token_scopes: Array.isArray(request.entry.token_scopes) && request.entry.token_scopes.length > 0 ? request.entry.token_scopes : ['unknown'],
419
+ tool_name: String(toolName),
420
+ args_shape: shapeObject(params.arguments || params.args || {}),
421
+ status,
422
+ duration_ms: Math.max(0, durationMs),
423
+ result_shape: resultShape,
424
+ error_class: response?.error ? String(response.error.code || response.error.message || 'mcp_error') : null,
425
+ }
426
+ }
427
+
428
+ function buildMcpUsageMetrics(toolCalls) {
429
+ const callsByStatus = new Map()
430
+ for (const call of toolCalls) {
431
+ const key = `${call.tool_name}:${call.status}:${call.token_scopes[0] || 'unknown'}`
432
+ callsByStatus.set(key, (callsByStatus.get(key) || 0) + 1)
433
+ }
434
+ const metrics = [...callsByStatus.entries()].map(([key, value]) => ({
435
+ name: 'mcp_tool_calls_total',
436
+ type: 'counter',
437
+ value: String(value),
438
+ labels: ['tool_name', 'status', 'token_scope'],
439
+ dimensions: key,
440
+ }))
441
+ const durations = toolCalls.filter((call) => call.duration_ms > 0)
442
+ if (durations.length > 0) {
443
+ metrics.push({
444
+ name: 'mcp_tool_call_duration_ms',
445
+ type: 'histogram',
446
+ value: String(Math.round(durations.reduce((sum, call) => sum + call.duration_ms, 0) / durations.length)),
447
+ labels: ['tool_name', 'status'],
448
+ })
449
+ }
450
+ return metrics.length > 0 ? metrics : [{ name: 'mcp_tool_calls_total', type: 'counter', value: '0', labels: ['tool_name', 'status'] }]
451
+ }
452
+
453
+ function durationBetween(start, end) {
454
+ if (!start || !end) return 0
455
+ const started = Date.parse(start)
456
+ const ended = Date.parse(end)
457
+ if (Number.isNaN(started) || Number.isNaN(ended) || ended < started) return 0
458
+ return ended - started
459
+ }
460
+
461
+ function privacyHash(value) {
462
+ return `sha256:${createHash('sha256').update(String(value)).digest('hex')}`
463
+ }
464
+
465
+ function shapeObject(value) {
466
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
467
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, shapeLabel(nested)]))
468
+ }
469
+
470
+ function shapeLabel(value) {
471
+ if (value === null) return 'null'
472
+ if (Array.isArray(value)) return `array:${value.length}`
473
+ if (typeof value === 'object') return `object:${Object.keys(value).length}`
474
+ if (typeof value === 'string') return looksSensitive(value) ? 'redacted_string' : 'string'
475
+ if (typeof value === 'number') return 'number'
476
+ if (typeof value === 'boolean') return 'boolean'
477
+ return typeof value
478
+ }
479
+
480
+ function looksSensitive(value) {
481
+ return /select\s|insert\s|update\s|delete\s|token|secret|password|bearer/i.test(value)
482
+ }
483
+
212
484
  export function validateMcpAuditReceipt(receipt) {
213
485
  const errors = []
214
486
  const warnings = []
@@ -307,3 +579,80 @@ export function validateMcpAuditReceipt(receipt) {
307
579
  },
308
580
  }
309
581
  }
582
+
583
+
584
+ export function validateToolSurfaceDiffReceipt(receipt) {
585
+ const errors = []
586
+ const warnings = []
587
+
588
+ function requireString(value, field) {
589
+ if (typeof value !== 'string' || value.trim() === '') errors.push(`${field} must be a non-empty string`)
590
+ }
591
+ function requireBoolean(value, field) {
592
+ if (typeof value !== 'boolean') errors.push(`${field} must be boolean`)
593
+ }
594
+ function requireNonNegativeInteger(value, field) {
595
+ if (!Number.isInteger(value) || value < 0) errors.push(`${field} must be a non-negative integer`)
596
+ }
597
+ function requireArray(value, field) {
598
+ if (!Array.isArray(value) || value.length === 0) errors.push(`${field} must be a non-empty array`)
599
+ }
600
+
601
+ if (receipt.schema !== TOOL_SURFACE_DIFF_SCHEMA) errors.push(`schema must be ${TOOL_SURFACE_DIFF_SCHEMA}`)
602
+ requireString(receipt.run_id, 'run_id')
603
+ requireString(receipt.generated_at, 'generated_at')
604
+ requireString(receipt.platform?.name, 'platform.name')
605
+ requireString(receipt.platform?.audit_sink, 'platform.audit_sink')
606
+ requireString(receipt.catalog?.server_id, 'catalog.server_id')
607
+ requireString(receipt.catalog?.previous_hash, 'catalog.previous_hash')
608
+ requireString(receipt.catalog?.current_hash, 'catalog.current_hash')
609
+ requireBoolean(receipt.runtime_discovery?.enabled, 'runtime_discovery.enabled')
610
+ requireString(receipt.runtime_discovery?.trigger, 'runtime_discovery.trigger')
611
+ requireArray(receipt.tools, 'tools')
612
+ requireString(receipt.privacy_boundary?.raw_schemas, 'privacy_boundary.raw_schemas')
613
+ requireString(receipt.privacy_boundary?.raw_prompts, 'privacy_boundary.raw_prompts')
614
+ requireString(receipt.privacy_boundary?.raw_results, 'privacy_boundary.raw_results')
615
+
616
+ if (receipt.privacy_boundary?.raw_schemas !== 'omitted_hash_only') errors.push('privacy_boundary.raw_schemas must be omitted_hash_only')
617
+ if (receipt.privacy_boundary?.raw_prompts !== 'omitted') errors.push('privacy_boundary.raw_prompts must be omitted')
618
+ if (receipt.privacy_boundary?.raw_results !== 'omitted') errors.push('privacy_boundary.raw_results must be omitted')
619
+
620
+ const statuses = new Set(['discovered', 'activated', 'withheld', 'blocked', 'removed'])
621
+ const outcomes = new Set(['accepted', 'blocked_by_rai', 'blocked_by_xpia', 'schema_invalid', 'entitlement_filtered', 'not_selected', 'removed'])
622
+ let discoveredCount = 0
623
+ let activatedCount = 0
624
+ let withheldCount = 0
625
+ let rawLeakCount = 0
626
+
627
+ for (const [index, tool] of (receipt.tools || []).entries()) {
628
+ const prefix = `tools[${index}]`
629
+ requireString(tool.tool_id, `${prefix}.tool_id`)
630
+ requireString(tool.name_hash, `${prefix}.name_hash`)
631
+ requireString(tool.schema_hash, `${prefix}.schema_hash`)
632
+ requireString(tool.status, `${prefix}.status`)
633
+ requireString(tool.validation_outcome, `${prefix}.validation_outcome`)
634
+ requireNonNegativeInteger(tool.diff_summary?.added_fields, `${prefix}.diff_summary.added_fields`)
635
+ requireNonNegativeInteger(tool.diff_summary?.removed_fields, `${prefix}.diff_summary.removed_fields`)
636
+ requireNonNegativeInteger(tool.diff_summary?.changed_fields, `${prefix}.diff_summary.changed_fields`)
637
+
638
+ if (!statuses.has(tool.status)) errors.push(`${prefix}.status must be one of ${[...statuses].join('|')}`)
639
+ if (!outcomes.has(tool.validation_outcome)) errors.push(`${prefix}.validation_outcome must be one of ${[...outcomes].join('|')}`)
640
+ if (!String(tool.name_hash || '').startsWith('sha256:')) errors.push(`${prefix}.name_hash must be a sha256: hash, not a raw tool name`)
641
+ if (!String(tool.schema_hash || '').startsWith('sha256:')) errors.push(`${prefix}.schema_hash must be a sha256: hash, not a raw schema`)
642
+ if (typeof tool.raw_schema === 'string' || typeof tool.description === 'string') rawLeakCount++
643
+
644
+ if (['discovered', 'activated', 'withheld', 'blocked'].includes(tool.status)) discoveredCount++
645
+ if (tool.status === 'activated') activatedCount++
646
+ if (['withheld', 'blocked'].includes(tool.status)) withheldCount++
647
+ }
648
+
649
+ if (rawLeakCount > 0) errors.push(`tools must not include raw_schema or description (${rawLeakCount} raw fields found)`)
650
+ if (activatedCount === 0) warnings.push('no activated tools recorded; receipt may only prove discovery/withholding')
651
+ if (withheldCount === 0) warnings.push('no withheld/blocked tools recorded; receipt does not prove negative space')
652
+
653
+ return {
654
+ errors,
655
+ warnings,
656
+ summary: { discoveredCount, activatedCount, withheldCount },
657
+ }
658
+ }
@@ -1 +1 @@
1
- export const VERSION = '0.3.38'
1
+ export const VERSION = '0.3.40'