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/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  initSigning,
5
5
  loadPolicy,
6
6
  validateCredentials
7
- } from "./chunk-ZCKNFULF.mjs";
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: "header",
148
- name: "Authorization",
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 + optional local signing
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
- Run with the generated policy file if you also want local signed receipts. Add --enforce when ready.
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");