protect-mcp 0.2.2 → 0.3.1
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/README.md +11 -1
- package/dist/bundle-TXOTFJIJ.mjs +8 -0
- package/dist/chunk-5JXFV37Y.mjs +53 -0
- package/dist/chunk-U7TMVD3E.mjs +1105 -0
- package/dist/cli.js +1176 -172
- package/dist/cli.mjs +462 -5
- package/dist/demo-server.d.mts +1 -0
- package/dist/demo-server.d.ts +1 -0
- package/dist/demo-server.js +137 -0
- package/dist/demo-server.mjs +136 -0
- package/dist/index.d.mts +83 -61
- package/dist/index.d.ts +83 -61
- package/dist/index.js +637 -271
- package/dist/index.mjs +7 -172
- package/package.json +3 -3
- package/dist/chunk-ZCKNFULF.mjs +0 -613
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
initSigning,
|
|
5
5
|
loadPolicy,
|
|
6
6
|
validateCredentials
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-U7TMVD3E.mjs";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
function printHelp() {
|
|
@@ -14,6 +14,11 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
|
14
14
|
Usage:
|
|
15
15
|
protect-mcp [options] -- <command> [args...]
|
|
16
16
|
protect-mcp init [--dir <path>]
|
|
17
|
+
protect-mcp demo
|
|
18
|
+
protect-mcp status [--dir <path>]
|
|
19
|
+
protect-mcp digest [--today] [--dir <path>]
|
|
20
|
+
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
21
|
+
protect-mcp bundle [--output <path>] [--dir <path>]
|
|
17
22
|
|
|
18
23
|
Options:
|
|
19
24
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -24,12 +29,22 @@ Options:
|
|
|
24
29
|
|
|
25
30
|
Commands:
|
|
26
31
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
32
|
+
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
33
|
+
status Show tool call statistics from the local decision log
|
|
34
|
+
digest Generate a human-readable summary of agent activity
|
|
35
|
+
receipts Show recent persisted signed receipts
|
|
36
|
+
bundle Export an offline-verifiable audit bundle
|
|
27
37
|
|
|
28
38
|
Examples:
|
|
29
39
|
protect-mcp -- node my-server.js
|
|
30
40
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
31
41
|
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
32
42
|
protect-mcp init
|
|
43
|
+
protect-mcp demo
|
|
44
|
+
protect-mcp status
|
|
45
|
+
protect-mcp digest --today
|
|
46
|
+
protect-mcp receipts --last 10
|
|
47
|
+
protect-mcp bundle --output audit.json
|
|
33
48
|
|
|
34
49
|
`);
|
|
35
50
|
}
|
|
@@ -144,19 +159,27 @@ async function handleInit(argv) {
|
|
|
144
159
|
},
|
|
145
160
|
credentials: {
|
|
146
161
|
_example_api: {
|
|
147
|
-
inject: "
|
|
148
|
-
name: "
|
|
162
|
+
inject: "env",
|
|
163
|
+
name: "EXAMPLE_API_KEY",
|
|
149
164
|
value_env: "EXAMPLE_API_KEY",
|
|
150
165
|
_comment: "Remove the underscore prefix and set EXAMPLE_API_KEY in your environment"
|
|
151
166
|
}
|
|
152
167
|
}
|
|
153
168
|
};
|
|
154
169
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
170
|
+
const claudeConfig = {
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"my-server": {
|
|
173
|
+
"command": "npx",
|
|
174
|
+
"args": ["protect-mcp", "--policy", configPath, "--", "node", "my-server.js"]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
155
178
|
process.stderr.write(`
|
|
156
179
|
${bold("protect-mcp initialized!")}
|
|
157
180
|
|
|
158
181
|
Created:
|
|
159
|
-
${configPath} Config with shadow mode +
|
|
182
|
+
${configPath} Config with shadow mode + local signing
|
|
160
183
|
${keyPath} Ed25519 signing keypair
|
|
161
184
|
|
|
162
185
|
${bold("Next steps:")}
|
|
@@ -170,13 +193,427 @@ ${bold("Your gateway public key:")}
|
|
|
170
193
|
${bold("Key ID (kid):")}
|
|
171
194
|
${keypair.kid}
|
|
172
195
|
|
|
196
|
+
${bold("Claude Desktop config snippet")} (add to claude_desktop_config.json):
|
|
197
|
+
${dim(JSON.stringify(claudeConfig, null, 2))}
|
|
198
|
+
|
|
199
|
+
${bold("Quick demo:")}
|
|
200
|
+
protect-mcp demo
|
|
201
|
+
|
|
173
202
|
Shadow mode is the default \u2014 all tool calls are logged and nothing is blocked.
|
|
174
|
-
|
|
203
|
+
Add --enforce when ready to block policy violations.
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
async function handleDemo() {
|
|
207
|
+
const { existsSync } = await import("fs");
|
|
208
|
+
const { join, dirname, resolve } = await import("path");
|
|
209
|
+
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
210
|
+
const cliDir = dirname(cliPath);
|
|
211
|
+
const demoServerPath = join(cliDir, "demo-server.js");
|
|
212
|
+
const configPath = join(process.cwd(), "protect-mcp.json");
|
|
213
|
+
const hasConfig = existsSync(configPath);
|
|
214
|
+
if (!hasConfig) {
|
|
215
|
+
process.stderr.write(`
|
|
216
|
+
${bold("protect-mcp demo")}
|
|
217
|
+
|
|
218
|
+
Starting demo with default shadow mode (no signing).
|
|
219
|
+
For signed receipts, run ${dim("npx protect-mcp init")} first.
|
|
220
|
+
|
|
221
|
+
`);
|
|
222
|
+
} else {
|
|
223
|
+
process.stderr.write(`
|
|
224
|
+
${bold("protect-mcp demo")}
|
|
225
|
+
|
|
226
|
+
Using config from ${configPath}
|
|
227
|
+
Starting demo server with 5 tools...
|
|
228
|
+
|
|
229
|
+
`);
|
|
230
|
+
}
|
|
231
|
+
let policy = null;
|
|
232
|
+
let policyDigest = "none";
|
|
233
|
+
let credentials;
|
|
234
|
+
let signing;
|
|
235
|
+
if (hasConfig) {
|
|
236
|
+
try {
|
|
237
|
+
const loaded = loadPolicy(configPath);
|
|
238
|
+
policy = loaded.policy;
|
|
239
|
+
policyDigest = loaded.digest;
|
|
240
|
+
credentials = loaded.credentials;
|
|
241
|
+
signing = loaded.signing;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
process.stderr.write(`[PROTECT_MCP] Warning: Could not load config: ${err instanceof Error ? err.message : err}
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (signing) {
|
|
248
|
+
const warnings = await initSigning(signing);
|
|
249
|
+
for (const w of warnings) {
|
|
250
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (credentials) {
|
|
255
|
+
const warnings = validateCredentials(credentials);
|
|
256
|
+
for (const w of warnings) {
|
|
257
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
258
|
+
`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const config = {
|
|
262
|
+
command: process.execPath,
|
|
263
|
+
// node
|
|
264
|
+
args: [demoServerPath],
|
|
265
|
+
policy,
|
|
266
|
+
policyDigest,
|
|
267
|
+
enforce: false,
|
|
268
|
+
// Demo always runs in shadow mode
|
|
269
|
+
verbose: true,
|
|
270
|
+
signing,
|
|
271
|
+
credentials
|
|
272
|
+
};
|
|
273
|
+
const gateway = new ProtectGateway(config);
|
|
274
|
+
process.stderr.write(`${bold("Demo ready!")} The demo server is running.
|
|
275
|
+
`);
|
|
276
|
+
process.stderr.write(`Send JSON-RPC tool calls on stdin, or use an MCP client.
|
|
277
|
+
|
|
278
|
+
`);
|
|
279
|
+
process.stderr.write(`${dim("Example (paste into stdin):")}
|
|
280
|
+
`);
|
|
281
|
+
process.stderr.write(`${dim('{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/etc/hosts"}}}')}
|
|
282
|
+
|
|
283
|
+
`);
|
|
284
|
+
await gateway.start();
|
|
285
|
+
}
|
|
286
|
+
async function handleStatus(argv) {
|
|
287
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
288
|
+
const { join } = await import("path");
|
|
289
|
+
let dir = process.cwd();
|
|
290
|
+
const dirIdx = argv.indexOf("--dir");
|
|
291
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) {
|
|
292
|
+
dir = argv[dirIdx + 1];
|
|
293
|
+
}
|
|
294
|
+
const logPath = join(dir, ".protect-mcp-log.jsonl");
|
|
295
|
+
if (!existsSync(logPath)) {
|
|
296
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
297
|
+
|
|
298
|
+
`);
|
|
299
|
+
process.stderr.write(`No log file found at ${logPath}
|
|
300
|
+
`);
|
|
301
|
+
process.stderr.write(`Run protect-mcp with a wrapped server first to generate logs.
|
|
302
|
+
`);
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
const raw = readFileSync(logPath, "utf-8");
|
|
306
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
307
|
+
if (lines.length === 0) {
|
|
308
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
309
|
+
|
|
310
|
+
No entries in log file.
|
|
311
|
+
`);
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
const entries = [];
|
|
315
|
+
for (const line of lines) {
|
|
316
|
+
try {
|
|
317
|
+
entries.push(JSON.parse(line));
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (entries.length === 0) {
|
|
322
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
323
|
+
|
|
324
|
+
No valid entries in log file.
|
|
325
|
+
`);
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
328
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
329
|
+
let allowCount = 0;
|
|
330
|
+
let denyCount = 0;
|
|
331
|
+
let rateLimitCount = 0;
|
|
332
|
+
const tierCounts = /* @__PURE__ */ new Map();
|
|
333
|
+
const reasonCounts = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
toolCounts.set(entry.tool, (toolCounts.get(entry.tool) || 0) + 1);
|
|
336
|
+
if (entry.decision === "allow") allowCount++;
|
|
337
|
+
else if (entry.decision === "deny") denyCount++;
|
|
338
|
+
if (entry.reason_code === "rate_limit_exceeded") rateLimitCount++;
|
|
339
|
+
if (entry.tier) tierCounts.set(entry.tier, (tierCounts.get(entry.tier) || 0) + 1);
|
|
340
|
+
reasonCounts.set(entry.reason_code, (reasonCounts.get(entry.reason_code) || 0) + 1);
|
|
341
|
+
}
|
|
342
|
+
const firstTs = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
343
|
+
const lastTs = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
344
|
+
const sortedTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
345
|
+
process.stdout.write(`
|
|
346
|
+
${bold("protect-mcp status")}
|
|
347
|
+
|
|
348
|
+
`);
|
|
349
|
+
process.stdout.write(` Total decisions: ${bold(String(entries.length))}
|
|
350
|
+
`);
|
|
351
|
+
process.stdout.write(` ${green("\u2713 Allow")}: ${allowCount} ${red("\u2717 Deny")}: ${denyCount} ${yellow("\u2298 Rate-limited")}: ${rateLimitCount}
|
|
352
|
+
|
|
353
|
+
`);
|
|
354
|
+
process.stdout.write(` ${bold("Time range:")}
|
|
355
|
+
`);
|
|
356
|
+
process.stdout.write(` First: ${firstTs.toISOString()}
|
|
357
|
+
`);
|
|
358
|
+
process.stdout.write(` Last: ${lastTs.toISOString()}
|
|
359
|
+
|
|
360
|
+
`);
|
|
361
|
+
process.stdout.write(` ${bold("Top tools:")}
|
|
362
|
+
`);
|
|
363
|
+
for (const [tool, count] of sortedTools.slice(0, 10)) {
|
|
364
|
+
const bar = "\u2588".repeat(Math.min(Math.ceil(count / entries.length * 30), 30));
|
|
365
|
+
process.stdout.write(` ${tool.padEnd(20)} ${String(count).padStart(4)} ${dim(bar)}
|
|
366
|
+
`);
|
|
367
|
+
}
|
|
368
|
+
if (tierCounts.size > 0) {
|
|
369
|
+
process.stdout.write(`
|
|
370
|
+
${bold("Trust tiers seen:")}
|
|
371
|
+
`);
|
|
372
|
+
for (const [tier, count] of tierCounts) {
|
|
373
|
+
process.stdout.write(` ${tier.padEnd(15)} ${count}
|
|
374
|
+
`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
process.stdout.write(`
|
|
378
|
+
${bold("Decision reasons:")}
|
|
379
|
+
`);
|
|
380
|
+
for (const [reason, count] of [...reasonCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
381
|
+
process.stdout.write(` ${reason.padEnd(25)} ${count}
|
|
382
|
+
`);
|
|
383
|
+
}
|
|
384
|
+
const evidencePath = join(dir, ".protect-mcp-evidence.json");
|
|
385
|
+
if (existsSync(evidencePath)) {
|
|
386
|
+
try {
|
|
387
|
+
const evidenceRaw = readFileSync(evidencePath, "utf-8");
|
|
388
|
+
const evidence = JSON.parse(evidenceRaw);
|
|
389
|
+
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
390
|
+
process.stdout.write(`
|
|
391
|
+
${bold("Evidence store:")} ${agentCount} agent(s) tracked
|
|
392
|
+
`);
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const keyPath = join(dir, "keys", "gateway.json");
|
|
397
|
+
if (existsSync(keyPath)) {
|
|
398
|
+
try {
|
|
399
|
+
const keyData = JSON.parse(readFileSync(keyPath, "utf-8"));
|
|
400
|
+
if (keyData.publicKey) {
|
|
401
|
+
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
402
|
+
process.stdout.write(`
|
|
403
|
+
${bold("\u{1F6E1}\uFE0F Passport identity:")}
|
|
404
|
+
`);
|
|
405
|
+
process.stdout.write(` Public key: ${fingerprint}
|
|
406
|
+
`);
|
|
407
|
+
if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
|
|
408
|
+
`);
|
|
409
|
+
process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
|
|
410
|
+
`);
|
|
411
|
+
process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
|
|
412
|
+
`);
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
process.stdout.write(`
|
|
418
|
+
Log file: ${dim(logPath)}
|
|
419
|
+
|
|
175
420
|
`);
|
|
176
421
|
}
|
|
177
422
|
function bold(s) {
|
|
178
423
|
return process.env.NO_COLOR ? s : `\x1B[1m${s}\x1B[0m`;
|
|
179
424
|
}
|
|
425
|
+
function dim(s) {
|
|
426
|
+
return process.env.NO_COLOR ? s : `\x1B[2m${s}\x1B[0m`;
|
|
427
|
+
}
|
|
428
|
+
function green(s) {
|
|
429
|
+
return process.env.NO_COLOR ? s : `\x1B[32m${s}\x1B[0m`;
|
|
430
|
+
}
|
|
431
|
+
function red(s) {
|
|
432
|
+
return process.env.NO_COLOR ? s : `\x1B[31m${s}\x1B[0m`;
|
|
433
|
+
}
|
|
434
|
+
function yellow(s) {
|
|
435
|
+
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
436
|
+
}
|
|
437
|
+
async function handleDigest(argv) {
|
|
438
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
439
|
+
const { join } = await import("path");
|
|
440
|
+
let dir = process.cwd();
|
|
441
|
+
const dirIdx = argv.indexOf("--dir");
|
|
442
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
443
|
+
const today = argv.includes("--today");
|
|
444
|
+
const logPath = join(dir, ".protect-mcp-log.jsonl");
|
|
445
|
+
if (!existsSync(logPath)) {
|
|
446
|
+
process.stderr.write(`${bold("protect-mcp digest")}
|
|
447
|
+
|
|
448
|
+
No log file found. Run protect-mcp first.
|
|
449
|
+
`);
|
|
450
|
+
process.exit(0);
|
|
451
|
+
}
|
|
452
|
+
const raw = readFileSync(logPath, "utf-8");
|
|
453
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
454
|
+
let entries = [];
|
|
455
|
+
for (const line of lines) {
|
|
456
|
+
try {
|
|
457
|
+
entries.push(JSON.parse(line));
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (today) {
|
|
462
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
463
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
464
|
+
entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
|
|
465
|
+
}
|
|
466
|
+
if (entries.length === 0) {
|
|
467
|
+
process.stdout.write(`
|
|
468
|
+
${bold("\u{1F6E1}\uFE0F Agent Digest")}
|
|
469
|
+
|
|
470
|
+
No activity${today ? " today" : ""}.
|
|
471
|
+
|
|
472
|
+
`);
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
476
|
+
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
477
|
+
const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
|
|
478
|
+
const toolUsage = /* @__PURE__ */ new Map();
|
|
479
|
+
for (const e of entries) {
|
|
480
|
+
toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
|
|
481
|
+
}
|
|
482
|
+
const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
|
|
483
|
+
const currentTier = entries[entries.length - 1]?.tier || "unknown";
|
|
484
|
+
const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
485
|
+
const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
486
|
+
const durationMs = lastTime.getTime() - firstTime.getTime();
|
|
487
|
+
const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
|
|
488
|
+
process.stdout.write(`
|
|
489
|
+
${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
490
|
+
|
|
491
|
+
`);
|
|
492
|
+
process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
|
|
493
|
+
process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
|
|
494
|
+
process.stdout.write(`${red("\u2717 " + denied)} blocked`);
|
|
495
|
+
if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
|
|
496
|
+
process.stdout.write(`
|
|
497
|
+
`);
|
|
498
|
+
process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
|
|
499
|
+
|
|
500
|
+
`);
|
|
501
|
+
process.stdout.write(` ${bold("Tools used:")}
|
|
502
|
+
`);
|
|
503
|
+
for (const [tool, count] of sortedTools.slice(0, 8)) {
|
|
504
|
+
process.stdout.write(` ${tool.padEnd(22)} ${count}x
|
|
505
|
+
`);
|
|
506
|
+
}
|
|
507
|
+
if (denied > 0) {
|
|
508
|
+
const deniedTools = entries.filter((e) => e.decision === "deny");
|
|
509
|
+
const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
|
|
510
|
+
process.stdout.write(`
|
|
511
|
+
${bold(red("Blocked tools:"))}
|
|
512
|
+
`);
|
|
513
|
+
for (const tool of deniedToolNames) {
|
|
514
|
+
const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
|
|
515
|
+
process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
|
|
516
|
+
`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
process.stdout.write(`
|
|
520
|
+
${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
|
|
521
|
+
`);
|
|
522
|
+
process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
|
|
523
|
+
`);
|
|
524
|
+
process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
|
|
525
|
+
|
|
526
|
+
`);
|
|
527
|
+
}
|
|
528
|
+
async function handleReceipts(argv) {
|
|
529
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
530
|
+
const { join } = await import("path");
|
|
531
|
+
let dir = process.cwd();
|
|
532
|
+
const dirIdx = argv.indexOf("--dir");
|
|
533
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
534
|
+
const lastIdx = argv.indexOf("--last");
|
|
535
|
+
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
536
|
+
const receiptsPath = join(dir, ".protect-mcp-receipts.jsonl");
|
|
537
|
+
if (!existsSync(receiptsPath)) {
|
|
538
|
+
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
539
|
+
|
|
540
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
541
|
+
`);
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
const raw = readFileSync(receiptsPath, "utf-8");
|
|
545
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
546
|
+
const recent = lines.slice(-count);
|
|
547
|
+
process.stdout.write(`
|
|
548
|
+
${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
549
|
+
|
|
550
|
+
`);
|
|
551
|
+
for (const line of recent) {
|
|
552
|
+
try {
|
|
553
|
+
const entry = JSON.parse(line);
|
|
554
|
+
const payload = entry.payload || {};
|
|
555
|
+
const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
|
|
556
|
+
const decision = payload.decision || "unknown";
|
|
557
|
+
const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
|
|
558
|
+
process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
|
|
559
|
+
`);
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
process.stdout.write(`
|
|
564
|
+
`);
|
|
565
|
+
}
|
|
566
|
+
async function handleBundle(argv) {
|
|
567
|
+
const { readFileSync, writeFileSync, existsSync } = await import("fs");
|
|
568
|
+
const { join } = await import("path");
|
|
569
|
+
const { createAuditBundle } = await import("./bundle-TXOTFJIJ.mjs");
|
|
570
|
+
let dir = process.cwd();
|
|
571
|
+
const dirIdx = argv.indexOf("--dir");
|
|
572
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
573
|
+
const outputIdx = argv.indexOf("--output");
|
|
574
|
+
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join(dir, "audit-bundle.json");
|
|
575
|
+
const receiptsPath = join(dir, ".protect-mcp-receipts.jsonl");
|
|
576
|
+
const keyPath = join(dir, "keys", "gateway.json");
|
|
577
|
+
if (!existsSync(receiptsPath)) {
|
|
578
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
579
|
+
|
|
580
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
581
|
+
`);
|
|
582
|
+
process.exit(0);
|
|
583
|
+
}
|
|
584
|
+
if (!existsSync(keyPath)) {
|
|
585
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
586
|
+
|
|
587
|
+
No key file found at ${keyPath}
|
|
588
|
+
`);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const receipts = readFileSync(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
592
|
+
const keyData = JSON.parse(readFileSync(keyPath, "utf-8"));
|
|
593
|
+
const bundle = createAuditBundle({
|
|
594
|
+
tenant: keyData.issuer || "protect-mcp",
|
|
595
|
+
receipts,
|
|
596
|
+
signingKeys: [{
|
|
597
|
+
kty: "OKP",
|
|
598
|
+
crv: "Ed25519",
|
|
599
|
+
kid: keyData.kid || "unknown",
|
|
600
|
+
x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
|
|
601
|
+
use: "sig"
|
|
602
|
+
}]
|
|
603
|
+
});
|
|
604
|
+
writeFileSync(outputPath, JSON.stringify(bundle, null, 2) + "\n");
|
|
605
|
+
process.stdout.write(`
|
|
606
|
+
${bold("protect-mcp bundle")}
|
|
607
|
+
|
|
608
|
+
`);
|
|
609
|
+
process.stdout.write(` Receipts: ${receipts.length}
|
|
610
|
+
`);
|
|
611
|
+
process.stdout.write(` Output: ${outputPath}
|
|
612
|
+
`);
|
|
613
|
+
process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
|
|
614
|
+
|
|
615
|
+
`);
|
|
616
|
+
}
|
|
180
617
|
async function main() {
|
|
181
618
|
const args = process.argv.slice(2);
|
|
182
619
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -187,6 +624,26 @@ async function main() {
|
|
|
187
624
|
await handleInit(args.slice(1));
|
|
188
625
|
process.exit(0);
|
|
189
626
|
}
|
|
627
|
+
if (args[0] === "demo") {
|
|
628
|
+
await handleDemo();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (args[0] === "status") {
|
|
632
|
+
await handleStatus(args.slice(1));
|
|
633
|
+
process.exit(0);
|
|
634
|
+
}
|
|
635
|
+
if (args[0] === "digest") {
|
|
636
|
+
await handleDigest(args.slice(1));
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
639
|
+
if (args[0] === "receipts") {
|
|
640
|
+
await handleReceipts(args.slice(1));
|
|
641
|
+
process.exit(0);
|
|
642
|
+
}
|
|
643
|
+
if (args[0] === "bundle") {
|
|
644
|
+
await handleBundle(args.slice(1));
|
|
645
|
+
process.exit(0);
|
|
646
|
+
}
|
|
190
647
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
191
648
|
let policy = null;
|
|
192
649
|
let policyDigest = "none";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/demo-server.ts
|
|
5
|
+
var import_node_readline = require("readline");
|
|
6
|
+
var TOOLS = [
|
|
7
|
+
{
|
|
8
|
+
name: "read_file",
|
|
9
|
+
description: "Read the contents of a file",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { path: { type: "string", description: "File path to read" } },
|
|
13
|
+
required: ["path"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "write_file",
|
|
18
|
+
description: "Write content to a file",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
path: { type: "string", description: "File path to write" },
|
|
23
|
+
content: { type: "string", description: "Content to write" }
|
|
24
|
+
},
|
|
25
|
+
required: ["path", "content"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "delete_file",
|
|
30
|
+
description: "Delete a file from the filesystem",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { path: { type: "string", description: "File path to delete" } },
|
|
34
|
+
required: ["path"]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "web_search",
|
|
39
|
+
description: "Search the web for information",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: { query: { type: "string", description: "Search query" } },
|
|
43
|
+
required: ["query"]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "deploy",
|
|
48
|
+
description: "Deploy the application to production",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
environment: { type: "string", description: "Target environment", enum: ["staging", "production"] },
|
|
53
|
+
reason: { type: "string", description: "Deployment reason" }
|
|
54
|
+
},
|
|
55
|
+
required: ["environment"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
function handleRequest(request) {
|
|
60
|
+
if (request.method === "initialize") {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
jsonrpc: "2.0",
|
|
63
|
+
id: request.id,
|
|
64
|
+
result: {
|
|
65
|
+
protocolVersion: "2024-11-05",
|
|
66
|
+
serverInfo: { name: "protect-mcp-demo", version: "0.2.0" },
|
|
67
|
+
capabilities: { tools: {} }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (request.method === "notifications/initialized") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
if (request.method === "tools/list") {
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
id: request.id,
|
|
78
|
+
result: { tools: TOOLS }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (request.method === "tools/call") {
|
|
82
|
+
const toolName = request.params?.name || "unknown";
|
|
83
|
+
const args = request.params?.arguments || {};
|
|
84
|
+
let resultText;
|
|
85
|
+
switch (toolName) {
|
|
86
|
+
case "read_file":
|
|
87
|
+
resultText = `[demo] Read file: ${args.path || "/example.txt"}
|
|
88
|
+
Contents: Hello from protect-mcp demo server!`;
|
|
89
|
+
break;
|
|
90
|
+
case "write_file":
|
|
91
|
+
resultText = `[demo] Wrote ${String(args.content || "").length} bytes to ${args.path || "/example.txt"}`;
|
|
92
|
+
break;
|
|
93
|
+
case "delete_file":
|
|
94
|
+
resultText = `[demo] Deleted file: ${args.path || "/example.txt"}`;
|
|
95
|
+
break;
|
|
96
|
+
case "web_search":
|
|
97
|
+
resultText = `[demo] Search results for "${args.query || "test"}":
|
|
98
|
+
1. Example result \u2014 scopeblind.com
|
|
99
|
+
2. MCP security \u2014 modelcontextprotocol.io`;
|
|
100
|
+
break;
|
|
101
|
+
case "deploy":
|
|
102
|
+
resultText = `[demo] Deployed to ${args.environment || "staging"}${args.reason ? ` (reason: ${args.reason})` : ""}`;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
resultText = `[demo] Unknown tool: ${toolName}`;
|
|
106
|
+
}
|
|
107
|
+
return JSON.stringify({
|
|
108
|
+
jsonrpc: "2.0",
|
|
109
|
+
id: request.id,
|
|
110
|
+
result: {
|
|
111
|
+
content: [{ type: "text", text: resultText }]
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (request.id !== void 0) {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
jsonrpc: "2.0",
|
|
118
|
+
id: request.id,
|
|
119
|
+
error: { code: -32601, message: `Method not found: ${request.method}` }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
var rl = (0, import_node_readline.createInterface)({ input: process.stdin, crlfDelay: Infinity });
|
|
125
|
+
rl.on("line", (line) => {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (!trimmed) return;
|
|
128
|
+
try {
|
|
129
|
+
const request = JSON.parse(trimmed);
|
|
130
|
+
const response = handleRequest(request);
|
|
131
|
+
if (response) {
|
|
132
|
+
process.stdout.write(response + "\n");
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
process.stderr.write("[DEMO_SERVER] protect-mcp demo server started \u2014 5 tools registered\n");
|