protect-mcp 0.3.1 → 0.3.2

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.js CHANGED
@@ -2697,6 +2697,178 @@ var init_bundle = __esm({
2697
2697
  }
2698
2698
  });
2699
2699
 
2700
+ // src/report.ts
2701
+ var report_exports = {};
2702
+ __export(report_exports, {
2703
+ formatReportMarkdown: () => formatReportMarkdown,
2704
+ generateReport: () => generateReport
2705
+ });
2706
+ function generateReport(logPath, receiptPath, periodDays) {
2707
+ const now = /* @__PURE__ */ new Date();
2708
+ const from = new Date(now.getTime() - periodDays * 864e5);
2709
+ const entries = [];
2710
+ if ((0, import_node_fs7.existsSync)(logPath)) {
2711
+ const raw = (0, import_node_fs7.readFileSync)(logPath, "utf-8");
2712
+ for (const line of raw.split("\n")) {
2713
+ const trimmed = line.trim();
2714
+ if (!trimmed) continue;
2715
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
2716
+ try {
2717
+ const parsed = JSON.parse(jsonStr);
2718
+ if (parsed.tool && parsed.decision && parsed.timestamp) {
2719
+ const entryTime = typeof parsed.timestamp === "number" && parsed.timestamp > 1e12 ? parsed.timestamp : parsed.timestamp * 1e3;
2720
+ if (entryTime >= from.getTime()) {
2721
+ entries.push(parsed);
2722
+ }
2723
+ }
2724
+ } catch {
2725
+ }
2726
+ }
2727
+ }
2728
+ let receiptsSigned = 0;
2729
+ let signerKid = "";
2730
+ let signerIssuer = "";
2731
+ if ((0, import_node_fs7.existsSync)(receiptPath)) {
2732
+ const raw = (0, import_node_fs7.readFileSync)(receiptPath, "utf-8");
2733
+ for (const line of raw.split("\n")) {
2734
+ const trimmed = line.trim();
2735
+ if (!trimmed) continue;
2736
+ try {
2737
+ const parsed = JSON.parse(trimmed);
2738
+ if (parsed.signature) {
2739
+ receiptsSigned++;
2740
+ if (parsed.kid && !signerKid) signerKid = parsed.kid;
2741
+ if (parsed.issuer && !signerIssuer) signerIssuer = parsed.issuer;
2742
+ }
2743
+ } catch {
2744
+ }
2745
+ }
2746
+ }
2747
+ const toolMap = /* @__PURE__ */ new Map();
2748
+ const tiers = /* @__PURE__ */ new Set();
2749
+ const policyDigests = /* @__PURE__ */ new Map();
2750
+ let allowed = 0;
2751
+ let blocked = 0;
2752
+ let rateLimited = 0;
2753
+ let approvalRequired = 0;
2754
+ for (const entry of entries) {
2755
+ const tool = entry.tool;
2756
+ if (!toolMap.has(tool)) {
2757
+ toolMap.set(tool, { total: 0, allowed: 0, blocked: 0, rate_limited: 0, approval_required: 0 });
2758
+ }
2759
+ const tm = toolMap.get(tool);
2760
+ tm.total++;
2761
+ if (entry.decision === "allow") {
2762
+ allowed++;
2763
+ tm.allowed++;
2764
+ } else if (entry.decision === "deny" && entry.reason_code === "rate_limit_exceeded") {
2765
+ rateLimited++;
2766
+ tm.rate_limited++;
2767
+ } else if (entry.decision === "deny" && entry.reason_code === "require_approval") {
2768
+ approvalRequired++;
2769
+ tm.approval_required++;
2770
+ } else {
2771
+ blocked++;
2772
+ tm.blocked++;
2773
+ }
2774
+ if (entry.tier) tiers.add(entry.tier);
2775
+ if (entry.policy_digest && !policyDigests.has(entry.policy_digest)) {
2776
+ policyDigests.set(entry.policy_digest, new Date(entry.timestamp).toISOString());
2777
+ }
2778
+ }
2779
+ const policyChanges = Array.from(policyDigests.entries()).map(([digest, at]) => ({
2780
+ at,
2781
+ policy_digest: digest
2782
+ })).sort((a, b) => a.at.localeCompare(b.at));
2783
+ return {
2784
+ generated_at: now.toISOString(),
2785
+ period: { from: from.toISOString(), to: now.toISOString() },
2786
+ signing_identity: signerKid ? { kid: signerKid, issuer: signerIssuer } : null,
2787
+ summary: {
2788
+ total_decisions: entries.length,
2789
+ allowed,
2790
+ blocked,
2791
+ rate_limited: rateLimited,
2792
+ approval_required: approvalRequired,
2793
+ unique_tools: toolMap.size,
2794
+ unique_tiers: tiers.size
2795
+ },
2796
+ tool_breakdown: Array.from(toolMap.entries()).map(([tool, stats]) => ({ tool, ...stats })).sort((a, b) => b.total - a.total),
2797
+ policy_changes: policyChanges,
2798
+ verification: {
2799
+ receipts_signed: receiptsSigned,
2800
+ receipts_unsigned: entries.length - receiptsSigned,
2801
+ verify_command: "npx @veritasacta/verify audit-bundle.json --bundle"
2802
+ }
2803
+ };
2804
+ }
2805
+ function formatReportMarkdown(report) {
2806
+ const lines = [];
2807
+ lines.push("# ScopeBlind Compliance Report");
2808
+ lines.push("");
2809
+ lines.push(`**Generated:** ${report.generated_at}`);
2810
+ lines.push(`**Period:** ${report.period.from.split("T")[0]} to ${report.period.to.split("T")[0]}`);
2811
+ if (report.signing_identity) {
2812
+ lines.push(`**Signing identity:** kid \`${report.signing_identity.kid}\`, issuer \`${report.signing_identity.issuer}\``);
2813
+ }
2814
+ lines.push("");
2815
+ lines.push("## Summary");
2816
+ lines.push("");
2817
+ lines.push(`| Metric | Value |`);
2818
+ lines.push(`|--------|-------|`);
2819
+ lines.push(`| Total decisions | ${report.summary.total_decisions} |`);
2820
+ lines.push(`| Allowed | ${report.summary.allowed} |`);
2821
+ lines.push(`| Blocked | ${report.summary.blocked} |`);
2822
+ lines.push(`| Rate-limited | ${report.summary.rate_limited} |`);
2823
+ lines.push(`| Approval required | ${report.summary.approval_required} |`);
2824
+ lines.push(`| Unique tools | ${report.summary.unique_tools} |`);
2825
+ lines.push(`| Unique tiers | ${report.summary.unique_tiers} |`);
2826
+ lines.push("");
2827
+ if (report.tool_breakdown.length > 0) {
2828
+ lines.push("## Tool Breakdown");
2829
+ lines.push("");
2830
+ lines.push("| Tool | Total | Allowed | Blocked | Rate-limited | Approval |");
2831
+ lines.push("|------|-------|---------|---------|--------------|----------|");
2832
+ for (const t of report.tool_breakdown) {
2833
+ lines.push(`| \`${t.tool}\` | ${t.total} | ${t.allowed} | ${t.blocked} | ${t.rate_limited} | ${t.approval_required} |`);
2834
+ }
2835
+ lines.push("");
2836
+ }
2837
+ if (report.policy_changes.length > 0) {
2838
+ lines.push("## Policy History");
2839
+ lines.push("");
2840
+ lines.push("| Timestamp | Policy Digest |");
2841
+ lines.push("|-----------|--------------|");
2842
+ for (const pc of report.policy_changes) {
2843
+ lines.push(`| ${pc.at} | \`${pc.policy_digest}\` |`);
2844
+ }
2845
+ lines.push("");
2846
+ }
2847
+ lines.push("## Verification");
2848
+ lines.push("");
2849
+ lines.push(`- Receipts signed: **${report.verification.receipts_signed}**`);
2850
+ lines.push(`- Receipts unsigned: **${report.verification.receipts_unsigned}**`);
2851
+ lines.push("");
2852
+ lines.push("Verify the audit bundle:");
2853
+ lines.push("");
2854
+ lines.push("```bash");
2855
+ lines.push(report.verification.verify_command);
2856
+ lines.push("```");
2857
+ lines.push("");
2858
+ lines.push("The verifier is MIT-licensed and works offline. No ScopeBlind account required.");
2859
+ lines.push("");
2860
+ lines.push("---");
2861
+ lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
2862
+ return lines.join("\n");
2863
+ }
2864
+ var import_node_fs7;
2865
+ var init_report = __esm({
2866
+ "src/report.ts"() {
2867
+ "use strict";
2868
+ import_node_fs7 = require("fs");
2869
+ }
2870
+ });
2871
+
2700
2872
  // src/gateway.ts
2701
2873
  var import_node_child_process = require("child_process");
2702
2874
  var import_node_crypto2 = require("crypto");
@@ -3019,7 +3191,11 @@ async function initSigning(config) {
3019
3191
  return warnings;
3020
3192
  }
3021
3193
  try {
3022
- artifactsModule = await import("@veritasacta/artifacts");
3194
+ const moduleName = "@veritasacta/artifacts";
3195
+ artifactsModule = await import(
3196
+ /* @vite-ignore */
3197
+ moduleName
3198
+ );
3023
3199
  } catch {
3024
3200
  warnings.push("signing: @veritasacta/artifacts not available \u2014 receipts will be unsigned");
3025
3201
  return warnings;
@@ -3596,8 +3772,31 @@ var ProtectGateway = class {
3596
3772
  async interceptToolCall(request) {
3597
3773
  const toolName = request.params?.name || "unknown";
3598
3774
  const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
3599
- const toolPolicy = getToolPolicy(toolName, this.config.policy);
3600
3775
  const mode = this.config.enforce ? "enforce" : "shadow";
3776
+ let resolvedAgentKid = this.admissionResult?.agent_id;
3777
+ let effectiveToolPolicy;
3778
+ if (this.config.multiAgent?.enabled) {
3779
+ const paramKid = request.params?._passport_kid;
3780
+ if (paramKid) resolvedAgentKid = paramKid;
3781
+ const agentOverrides = resolvedAgentKid ? this.config.multiAgent.agentPolicies?.[resolvedAgentKid] : void 0;
3782
+ if (agentOverrides && agentOverrides[toolName]) {
3783
+ effectiveToolPolicy = { ...getToolPolicy(toolName, this.config.policy), ...agentOverrides[toolName] };
3784
+ } else if (!resolvedAgentKid && this.config.multiAgent.unknownAgentPolicy === "deny") {
3785
+ this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: "unknown_agent_denied", request_id: requestId, tier: this.currentTier });
3786
+ if (this.config.enforce) {
3787
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied: unidentified agent`);
3788
+ }
3789
+ return null;
3790
+ } else {
3791
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
3792
+ }
3793
+ if (this.config.verbose && resolvedAgentKid) {
3794
+ this.log(`Multi-agent: resolved kid=${resolvedAgentKid} for tool=${toolName}`);
3795
+ }
3796
+ } else {
3797
+ effectiveToolPolicy = getToolPolicy(toolName, this.config.policy);
3798
+ }
3799
+ const toolPolicy = effectiveToolPolicy;
3601
3800
  let credentialRef;
3602
3801
  if (this.config.credentials) {
3603
3802
  const cred = resolveCredential(toolName, this.config.credentials);
@@ -3772,6 +3971,137 @@ var ProtectGateway = class {
3772
3971
  }
3773
3972
  };
3774
3973
 
3974
+ // src/simulate.ts
3975
+ var import_node_fs6 = require("fs");
3976
+ function parseLogFile(path) {
3977
+ const raw = (0, import_node_fs6.readFileSync)(path, "utf-8");
3978
+ const entries = [];
3979
+ for (const line of raw.split("\n")) {
3980
+ const trimmed = line.trim();
3981
+ if (!trimmed) continue;
3982
+ const jsonStr = trimmed.replace(/^\[PROTECT_MCP\]\s*/, "");
3983
+ try {
3984
+ const parsed = JSON.parse(jsonStr);
3985
+ if (parsed.tool && parsed.decision) {
3986
+ entries.push(parsed);
3987
+ }
3988
+ } catch {
3989
+ }
3990
+ }
3991
+ return entries;
3992
+ }
3993
+ function simulate(entries, policy, tier = "unknown") {
3994
+ const rateLimitStore = /* @__PURE__ */ new Map();
3995
+ const toolResults = /* @__PURE__ */ new Map();
3996
+ const totals = {
3997
+ allow: 0,
3998
+ block: 0,
3999
+ rate_limited: 0,
4000
+ require_approval: 0,
4001
+ tier_insufficient: 0
4002
+ };
4003
+ const originalTotals = { allow: 0, deny: 0 };
4004
+ const changes = [];
4005
+ for (const entry of entries) {
4006
+ const toolName = entry.tool;
4007
+ const toolPolicy = getToolPolicy(toolName, policy);
4008
+ if (entry.decision === "allow") {
4009
+ originalTotals.allow++;
4010
+ } else {
4011
+ originalTotals.deny++;
4012
+ }
4013
+ let newDecision;
4014
+ if (toolPolicy.block) {
4015
+ newDecision = "block";
4016
+ } else if (toolPolicy.min_tier && !meetsMinTier(tier, toolPolicy.min_tier)) {
4017
+ newDecision = "tier_insufficient";
4018
+ } else if (toolPolicy.require_approval) {
4019
+ newDecision = "require_approval";
4020
+ } else if (toolPolicy.rate_limit) {
4021
+ const limit = parseRateLimit(toolPolicy.rate_limit);
4022
+ const result = checkRateLimit(toolName, limit, rateLimitStore);
4023
+ newDecision = result.allowed ? "allow" : "rate_limited";
4024
+ } else {
4025
+ newDecision = "allow";
4026
+ }
4027
+ totals[newDecision]++;
4028
+ if (!toolResults.has(toolName)) {
4029
+ toolResults.set(toolName, {
4030
+ tool: toolName,
4031
+ calls: 0,
4032
+ results: { allow: 0, block: 0, rate_limited: 0, require_approval: 0, tier_insufficient: 0 },
4033
+ original: { allow: 0, deny: 0 }
4034
+ });
4035
+ }
4036
+ const tr = toolResults.get(toolName);
4037
+ tr.calls++;
4038
+ tr.results[newDecision]++;
4039
+ if (entry.decision === "allow") {
4040
+ tr.original.allow++;
4041
+ } else {
4042
+ tr.original.deny++;
4043
+ }
4044
+ }
4045
+ for (const [tool, result] of toolResults) {
4046
+ const wasAllBlocked = result.original.allow === 0;
4047
+ const nowAllBlocked = result.results.allow === 0;
4048
+ const wasAllAllowed = result.original.deny === 0;
4049
+ if (wasAllAllowed && result.results.block > 0) {
4050
+ changes.push(`${tool}: ${result.results.block} calls would be blocked (was: all allowed)`);
4051
+ }
4052
+ if (wasAllAllowed && result.results.rate_limited > 0) {
4053
+ changes.push(`${tool}: ${result.results.rate_limited} calls would be rate-limited (was: all allowed)`);
4054
+ }
4055
+ if (wasAllAllowed && result.results.require_approval > 0) {
4056
+ changes.push(`${tool}: ${result.results.require_approval} calls would require approval (was: all allowed)`);
4057
+ }
4058
+ if (wasAllAllowed && result.results.tier_insufficient > 0) {
4059
+ changes.push(`${tool}: ${result.results.tier_insufficient} calls would fail tier check (was: all allowed)`);
4060
+ }
4061
+ if (wasAllBlocked && result.results.allow > 0 && !nowAllBlocked) {
4062
+ changes.push(`${tool}: ${result.results.allow} calls would now be allowed (was: all blocked)`);
4063
+ }
4064
+ }
4065
+ return {
4066
+ policy_file: "",
4067
+ log_file: "",
4068
+ total_calls: entries.length,
4069
+ results: totals,
4070
+ original: originalTotals,
4071
+ tool_breakdown: Array.from(toolResults.values()).sort((a, b) => b.calls - a.calls),
4072
+ changes
4073
+ };
4074
+ }
4075
+ function formatSimulation(summary) {
4076
+ const lines = [];
4077
+ lines.push(`Simulating ${summary.policy_file} against ${summary.total_calls} recorded tool calls:
4078
+ `);
4079
+ const maxToolLen = Math.max(...summary.tool_breakdown.map((t) => t.tool.length), 4);
4080
+ for (const tr of summary.tool_breakdown) {
4081
+ const parts = [];
4082
+ if (tr.results.allow > 0) parts.push(`${tr.results.allow} allow`);
4083
+ if (tr.results.block > 0) parts.push(`\x1B[31m${tr.results.block} blocked\x1B[0m`);
4084
+ if (tr.results.rate_limited > 0) parts.push(`\x1B[33m${tr.results.rate_limited} rate_limited\x1B[0m`);
4085
+ if (tr.results.require_approval > 0) parts.push(`\x1B[36m${tr.results.require_approval} require_approval\x1B[0m`);
4086
+ if (tr.results.tier_insufficient > 0) parts.push(`\x1B[35m${tr.results.tier_insufficient} tier_insufficient\x1B[0m`);
4087
+ const originalParts = [];
4088
+ if (tr.original.allow > 0) originalParts.push(`${tr.original.allow} allow`);
4089
+ if (tr.original.deny > 0) originalParts.push(`${tr.original.deny} deny`);
4090
+ lines.push(` ${tr.tool.padEnd(maxToolLen)} \xD7 ${String(tr.calls).padStart(3)} \u2192 ${parts.join(", ")} (was: ${originalParts.join(", ")})`);
4091
+ }
4092
+ lines.push("");
4093
+ lines.push(`Summary: ${summary.results.allow} allow, ${summary.results.block} blocked, ${summary.results.rate_limited} rate_limited, ${summary.results.require_approval} require_approval, ${summary.results.tier_insufficient} tier_insufficient`);
4094
+ lines.push(` vs original: ${summary.original.allow} allow, ${summary.original.deny} deny`);
4095
+ if (summary.changes.length > 0) {
4096
+ lines.push("");
4097
+ lines.push("Changes:");
4098
+ for (const change of summary.changes) {
4099
+ lines.push(` \u2022 ${change}`);
4100
+ }
4101
+ }
4102
+ return lines.join("\n");
4103
+ }
4104
+
3775
4105
  // src/cli.ts
3776
4106
  function printHelp() {
3777
4107
  process.stderr.write(`
@@ -3779,12 +4109,16 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
3779
4109
 
3780
4110
  Usage:
3781
4111
  protect-mcp [options] -- <command> [args...]
4112
+ protect-mcp quickstart
3782
4113
  protect-mcp init [--dir <path>]
3783
4114
  protect-mcp demo
4115
+ protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
3784
4116
  protect-mcp status [--dir <path>]
3785
4117
  protect-mcp digest [--today] [--dir <path>]
3786
4118
  protect-mcp receipts [--last <n>] [--dir <path>]
3787
4119
  protect-mcp bundle [--output <path>] [--dir <path>]
4120
+ protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
4121
+ protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
3788
4122
 
3789
4123
  Options:
3790
4124
  --policy <path> Policy/config JSON file (default: allow-all)
@@ -3794,19 +4128,22 @@ Options:
3794
4128
  --help Show this help
3795
4129
 
3796
4130
  Commands:
4131
+ quickstart Zero-config onboarding: init + demo + show receipts in one command
3797
4132
  init Generate config template, Ed25519 keypair, and sample policy
3798
4133
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
4134
+ trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
3799
4135
  status Show tool call statistics from the local decision log
3800
4136
  digest Generate a human-readable summary of agent activity
3801
4137
  receipts Show recent persisted signed receipts
3802
4138
  bundle Export an offline-verifiable audit bundle
3803
4139
 
3804
4140
  Examples:
4141
+ protect-mcp quickstart
3805
4142
  protect-mcp -- node my-server.js
3806
4143
  protect-mcp --policy protect-mcp.json -- node my-server.js
3807
- protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
3808
4144
  protect-mcp init
3809
4145
  protect-mcp demo
4146
+ protect-mcp trace sha256:abc123 --depth 5
3810
4147
  protect-mcp status
3811
4148
  protect-mcp digest --today
3812
4149
  protect-mcp receipts --last 10
@@ -3854,7 +4191,7 @@ function parseArgs(argv) {
3854
4191
  return { policyPath, slug, enforce, verbose, childCommand };
3855
4192
  }
3856
4193
  async function handleInit(argv) {
3857
- const { writeFileSync: writeFileSync2, existsSync: existsSync4, mkdirSync } = await import("fs");
4194
+ const { writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync } = await import("fs");
3858
4195
  const { join: join4 } = await import("path");
3859
4196
  let dir = process.cwd();
3860
4197
  const dirIdx = argv.indexOf("--dir");
@@ -3864,17 +4201,14 @@ async function handleInit(argv) {
3864
4201
  const configPath = join4(dir, "protect-mcp.json");
3865
4202
  const keysDir = join4(dir, "keys");
3866
4203
  const keyPath = join4(keysDir, "gateway.json");
3867
- if (existsSync4(configPath)) {
4204
+ if (existsSync5(configPath)) {
3868
4205
  process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
3869
4206
  `);
3870
4207
  process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
3871
4208
  process.exit(1);
3872
4209
  }
3873
4210
  let keypair;
3874
- try {
3875
- const artifacts = await import("@veritasacta/artifacts");
3876
- keypair = artifacts.generateKeypair();
3877
- } catch {
4211
+ {
3878
4212
  const { randomBytes: randomBytes3 } = await import("crypto");
3879
4213
  const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
3880
4214
  const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
@@ -3886,7 +4220,7 @@ async function handleInit(argv) {
3886
4220
  kid: "generated"
3887
4221
  };
3888
4222
  }
3889
- if (!existsSync4(keysDir)) {
4223
+ if (!existsSync5(keysDir)) {
3890
4224
  mkdirSync(keysDir, { recursive: true });
3891
4225
  }
3892
4226
  writeFileSync2(keyPath, JSON.stringify({
@@ -3897,7 +4231,7 @@ async function handleInit(argv) {
3897
4231
  warning: "KEEP THIS FILE SECRET. Never commit to version control."
3898
4232
  }, null, 2) + "\n");
3899
4233
  const gitignorePath = join4(keysDir, ".gitignore");
3900
- if (!existsSync4(gitignorePath)) {
4234
+ if (!existsSync5(gitignorePath)) {
3901
4235
  writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
3902
4236
  }
3903
4237
  const config = {
@@ -3970,13 +4304,13 @@ Add --enforce when ready to block policy violations.
3970
4304
  `);
3971
4305
  }
3972
4306
  async function handleDemo() {
3973
- const { existsSync: existsSync4 } = await import("fs");
4307
+ const { existsSync: existsSync5 } = await import("fs");
3974
4308
  const { join: join4, dirname, resolve } = await import("path");
3975
4309
  const cliPath = resolve(process.argv[1] || "dist/cli.js");
3976
4310
  const cliDir = dirname(cliPath);
3977
4311
  const demoServerPath = join4(cliDir, "demo-server.js");
3978
4312
  const configPath = join4(process.cwd(), "protect-mcp.json");
3979
- const hasConfig = existsSync4(configPath);
4313
+ const hasConfig = existsSync5(configPath);
3980
4314
  if (!hasConfig) {
3981
4315
  process.stderr.write(`
3982
4316
  ${bold("protect-mcp demo")}
@@ -4050,7 +4384,7 @@ Starting demo server with 5 tools...
4050
4384
  await gateway.start();
4051
4385
  }
4052
4386
  async function handleStatus2(argv) {
4053
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4387
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4054
4388
  const { join: join4 } = await import("path");
4055
4389
  let dir = process.cwd();
4056
4390
  const dirIdx = argv.indexOf("--dir");
@@ -4058,7 +4392,7 @@ async function handleStatus2(argv) {
4058
4392
  dir = argv[dirIdx + 1];
4059
4393
  }
4060
4394
  const logPath = join4(dir, ".protect-mcp-log.jsonl");
4061
- if (!existsSync4(logPath)) {
4395
+ if (!existsSync5(logPath)) {
4062
4396
  process.stderr.write(`${bold("protect-mcp status")}
4063
4397
 
4064
4398
  `);
@@ -4068,7 +4402,7 @@ async function handleStatus2(argv) {
4068
4402
  `);
4069
4403
  process.exit(0);
4070
4404
  }
4071
- const raw = readFileSync5(logPath, "utf-8");
4405
+ const raw = readFileSync7(logPath, "utf-8");
4072
4406
  const lines = raw.trim().split("\n").filter(Boolean);
4073
4407
  if (lines.length === 0) {
4074
4408
  process.stderr.write(`${bold("protect-mcp status")}
@@ -4148,9 +4482,9 @@ ${bold("protect-mcp status")}
4148
4482
  `);
4149
4483
  }
4150
4484
  const evidencePath = join4(dir, ".protect-mcp-evidence.json");
4151
- if (existsSync4(evidencePath)) {
4485
+ if (existsSync5(evidencePath)) {
4152
4486
  try {
4153
- const evidenceRaw = readFileSync5(evidencePath, "utf-8");
4487
+ const evidenceRaw = readFileSync7(evidencePath, "utf-8");
4154
4488
  const evidence = JSON.parse(evidenceRaw);
4155
4489
  const agentCount = Object.keys(evidence.agents || {}).length;
4156
4490
  process.stdout.write(`
@@ -4160,9 +4494,9 @@ ${bold("protect-mcp status")}
4160
4494
  }
4161
4495
  }
4162
4496
  const keyPath = join4(dir, "keys", "gateway.json");
4163
- if (existsSync4(keyPath)) {
4497
+ if (existsSync5(keyPath)) {
4164
4498
  try {
4165
- const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
4499
+ const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
4166
4500
  if (keyData.publicKey) {
4167
4501
  const fingerprint = keyData.publicKey.slice(0, 16) + "...";
4168
4502
  process.stdout.write(`
@@ -4201,21 +4535,21 @@ function yellow(s) {
4201
4535
  return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
4202
4536
  }
4203
4537
  async function handleDigest(argv) {
4204
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4538
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4205
4539
  const { join: join4 } = await import("path");
4206
4540
  let dir = process.cwd();
4207
4541
  const dirIdx = argv.indexOf("--dir");
4208
4542
  if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
4209
4543
  const today = argv.includes("--today");
4210
4544
  const logPath = join4(dir, ".protect-mcp-log.jsonl");
4211
- if (!existsSync4(logPath)) {
4545
+ if (!existsSync5(logPath)) {
4212
4546
  process.stderr.write(`${bold("protect-mcp digest")}
4213
4547
 
4214
4548
  No log file found. Run protect-mcp first.
4215
4549
  `);
4216
4550
  process.exit(0);
4217
4551
  }
4218
- const raw = readFileSync5(logPath, "utf-8");
4552
+ const raw = readFileSync7(logPath, "utf-8");
4219
4553
  const lines = raw.trim().split("\n").filter(Boolean);
4220
4554
  let entries = [];
4221
4555
  for (const line of lines) {
@@ -4292,7 +4626,7 @@ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
4292
4626
  `);
4293
4627
  }
4294
4628
  async function handleReceipts2(argv) {
4295
- const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
4629
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4296
4630
  const { join: join4 } = await import("path");
4297
4631
  let dir = process.cwd();
4298
4632
  const dirIdx = argv.indexOf("--dir");
@@ -4300,14 +4634,14 @@ async function handleReceipts2(argv) {
4300
4634
  const lastIdx = argv.indexOf("--last");
4301
4635
  const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
4302
4636
  const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4303
- if (!existsSync4(receiptsPath)) {
4637
+ if (!existsSync5(receiptsPath)) {
4304
4638
  process.stderr.write(`${bold("protect-mcp receipts")}
4305
4639
 
4306
4640
  No signed receipt file found. Run protect-mcp with signing enabled first.
4307
4641
  `);
4308
4642
  process.exit(0);
4309
4643
  }
4310
- const raw = readFileSync5(receiptsPath, "utf-8");
4644
+ const raw = readFileSync7(receiptsPath, "utf-8");
4311
4645
  const lines = raw.trim().split("\n").filter(Boolean);
4312
4646
  const recent = lines.slice(-count);
4313
4647
  process.stdout.write(`
@@ -4330,7 +4664,7 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
4330
4664
  `);
4331
4665
  }
4332
4666
  async function handleBundle(argv) {
4333
- const { readFileSync: readFileSync5, writeFileSync: writeFileSync2, existsSync: existsSync4 } = await import("fs");
4667
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync2, existsSync: existsSync5 } = await import("fs");
4334
4668
  const { join: join4 } = await import("path");
4335
4669
  const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
4336
4670
  let dir = process.cwd();
@@ -4340,22 +4674,22 @@ async function handleBundle(argv) {
4340
4674
  const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
4341
4675
  const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
4342
4676
  const keyPath = join4(dir, "keys", "gateway.json");
4343
- if (!existsSync4(receiptsPath)) {
4677
+ if (!existsSync5(receiptsPath)) {
4344
4678
  process.stderr.write(`${bold("protect-mcp bundle")}
4345
4679
 
4346
4680
  No signed receipt file found. Run protect-mcp with signing enabled first.
4347
4681
  `);
4348
4682
  process.exit(0);
4349
4683
  }
4350
- if (!existsSync4(keyPath)) {
4684
+ if (!existsSync5(keyPath)) {
4351
4685
  process.stderr.write(`${bold("protect-mcp bundle")}
4352
4686
 
4353
4687
  No key file found at ${keyPath}
4354
4688
  `);
4355
4689
  process.exit(1);
4356
4690
  }
4357
- const receipts = readFileSync5(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
4358
- const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
4691
+ const receipts = readFileSync7(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
4692
+ const keyData = JSON.parse(readFileSync7(keyPath, "utf-8"));
4359
4693
  const bundle = createAuditBundle2({
4360
4694
  tenant: keyData.issuer || "protect-mcp",
4361
4695
  receipts,
@@ -4380,12 +4714,322 @@ ${bold("protect-mcp bundle")}
4380
4714
 
4381
4715
  `);
4382
4716
  }
4717
+ async function handleQuickstart() {
4718
+ const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync5, mkdirSync, readFileSync: readFileSync7 } = await import("fs");
4719
+ const { join: join4 } = await import("path");
4720
+ const { tmpdir } = await import("os");
4721
+ const dir = mkdtempSync(join4(tmpdir(), "protect-mcp-quickstart-"));
4722
+ process.stdout.write(`
4723
+ ${bold("protect-mcp quickstart")}
4724
+ `);
4725
+ process.stdout.write(`${"\u2500".repeat(50)}
4726
+
4727
+ `);
4728
+ process.stdout.write(` This will:
4729
+ `);
4730
+ process.stdout.write(` 1. Generate an Ed25519 signing keypair
4731
+ `);
4732
+ process.stdout.write(` 2. Create a shadow-mode policy
4733
+ `);
4734
+ process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
4735
+ `);
4736
+ process.stdout.write(` 4. Log signed receipts for every tool call
4737
+
4738
+ `);
4739
+ process.stdout.write(` Working dir: ${dir}
4740
+
4741
+ `);
4742
+ const keysDir = join4(dir, "keys");
4743
+ mkdirSync(keysDir, { recursive: true });
4744
+ const { randomBytes: randomBytes3 } = await import("crypto");
4745
+ let keypair;
4746
+ try {
4747
+ const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
4748
+ const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
4749
+ const privateKey = randomBytes3(32);
4750
+ const publicKey = ed255192.getPublicKey(privateKey);
4751
+ keypair = {
4752
+ privateKey: bytesToHex2(privateKey),
4753
+ publicKey: bytesToHex2(publicKey),
4754
+ kid: `quickstart-${Date.now()}`
4755
+ };
4756
+ } catch {
4757
+ keypair = {
4758
+ privateKey: randomBytes3(32).toString("hex"),
4759
+ publicKey: randomBytes3(32).toString("hex"),
4760
+ kid: `quickstart-${Date.now()}`
4761
+ };
4762
+ }
4763
+ writeFileSync2(join4(keysDir, "gateway.json"), JSON.stringify({
4764
+ privateKey: keypair.privateKey,
4765
+ publicKey: keypair.publicKey,
4766
+ kid: keypair.kid,
4767
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
4768
+ }, null, 2) + "\n");
4769
+ const configPath = join4(dir, "protect-mcp.json");
4770
+ const config = {
4771
+ tools: {
4772
+ "*": { rate_limit: "100/hour" },
4773
+ "delete_file": { block: true }
4774
+ },
4775
+ default_tier: "unknown",
4776
+ signing: {
4777
+ key_path: join4(keysDir, "gateway.json"),
4778
+ issuer: "protect-mcp-quickstart",
4779
+ enabled: true
4780
+ }
4781
+ };
4782
+ writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n");
4783
+ process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
4784
+ `);
4785
+ process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
4786
+ `);
4787
+ process.stdout.write(` \u2713 Signing enabled (Ed25519)
4788
+
4789
+ `);
4790
+ process.stdout.write(`${bold("Starting demo server...")}
4791
+
4792
+ `);
4793
+ process.stdout.write(` Every tool call will produce a signed receipt.
4794
+ `);
4795
+ process.stdout.write(` Try it with Claude Desktop or any MCP client.
4796
+
4797
+ `);
4798
+ process.stdout.write(` ${bold("To use in production:")}
4799
+ `);
4800
+ process.stdout.write(` 1. Copy ${configPath} to your project
4801
+ `);
4802
+ process.stdout.write(` 2. Edit tool policies to match your server
4803
+ `);
4804
+ process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
4805
+
4806
+ `);
4807
+ process.stdout.write(`${"\u2500".repeat(50)}
4808
+
4809
+ `);
4810
+ process.env.PROTECT_MCP_CONFIG = configPath;
4811
+ await handleDemo();
4812
+ }
4813
+ async function handleTrace(argv) {
4814
+ const receiptId = argv[0];
4815
+ if (!receiptId) {
4816
+ process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
4817
+ process.exit(1);
4818
+ }
4819
+ let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
4820
+ let depth = 3;
4821
+ for (let i = 1; i < argv.length; i++) {
4822
+ if (argv[i] === "--endpoint" && argv[i + 1]) {
4823
+ endpoint = argv[++i];
4824
+ } else if (argv[i] === "--depth" && argv[i + 1]) {
4825
+ depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
4826
+ }
4827
+ }
4828
+ process.stdout.write(`
4829
+ ${bold("protect-mcp trace")}
4830
+ `);
4831
+ process.stdout.write(`${"\u2500".repeat(60)}
4832
+
4833
+ `);
4834
+ process.stdout.write(` Root: ${receiptId}
4835
+ `);
4836
+ process.stdout.write(` Endpoint: ${endpoint}
4837
+ `);
4838
+ process.stdout.write(` Depth: ${depth}
4839
+
4840
+ `);
4841
+ const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
4842
+ let graphData;
4843
+ try {
4844
+ const resp = await fetch(url);
4845
+ if (!resp.ok) {
4846
+ const body = await resp.text();
4847
+ process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
4848
+ `);
4849
+ process.exit(1);
4850
+ }
4851
+ graphData = await resp.json();
4852
+ } catch (err) {
4853
+ process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
4854
+ `);
4855
+ process.stderr.write(`[PROTECT_MCP] Trying local receipts...
4856
+
4857
+ `);
4858
+ await traceLocal(receiptId);
4859
+ return;
4860
+ }
4861
+ if (!graphData.nodes || graphData.nodes.length === 0) {
4862
+ process.stdout.write(` No receipts found for ${receiptId}
4863
+
4864
+ `);
4865
+ return;
4866
+ }
4867
+ process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
4868
+
4869
+ `);
4870
+ const nodeMap = /* @__PURE__ */ new Map();
4871
+ for (const node of graphData.nodes) {
4872
+ nodeMap.set(node.receipt_id, node);
4873
+ }
4874
+ const childMap = /* @__PURE__ */ new Map();
4875
+ for (const edge of graphData.edges) {
4876
+ if (!childMap.has(edge.from)) childMap.set(edge.from, []);
4877
+ childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
4878
+ }
4879
+ const rendered = /* @__PURE__ */ new Set();
4880
+ function renderNode(id, prefix, isLast) {
4881
+ const node = nodeMap.get(id);
4882
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4883
+ const childPrefix = isLast ? " " : "\u2502 ";
4884
+ const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
4885
+ const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
4886
+ const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
4887
+ const type = node?.receipt_type?.replace("acta:", "") || "unknown";
4888
+ process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
4889
+ `);
4890
+ if (rendered.has(id)) {
4891
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
4892
+ `);
4893
+ return;
4894
+ }
4895
+ rendered.add(id);
4896
+ const children = childMap.get(id) || [];
4897
+ for (let i = 0; i < children.length; i++) {
4898
+ const child = children[i];
4899
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
4900
+ process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
4901
+ `);
4902
+ renderNode(child.to, prefix + childPrefix, i === children.length - 1);
4903
+ }
4904
+ }
4905
+ const rootNode = nodeMap.get(receiptId);
4906
+ if (rootNode) {
4907
+ const typeEmoji = getTypeEmoji(rootNode.receipt_type);
4908
+ const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
4909
+ const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
4910
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
4911
+ `);
4912
+ rendered.add(receiptId);
4913
+ const children = childMap.get(receiptId) || [];
4914
+ for (let i = 0; i < children.length; i++) {
4915
+ const child = children[i];
4916
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
4917
+ process.stdout.write(` ${edgeLabel}
4918
+ `);
4919
+ renderNode(child.to, " ", i === children.length - 1);
4920
+ }
4921
+ const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
4922
+ if (incomingEdges.length > 0) {
4923
+ process.stdout.write(`
4924
+ ${bold("Incoming edges:")}
4925
+ `);
4926
+ for (const edge of incomingEdges) {
4927
+ const fromNode = nodeMap.get(edge.from);
4928
+ const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
4929
+ process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
4930
+ `);
4931
+ }
4932
+ }
4933
+ } else {
4934
+ for (const node of graphData.nodes) {
4935
+ const typeEmoji = getTypeEmoji(node.receipt_type);
4936
+ const type = node.receipt_type?.replace("acta:", "") || "unknown";
4937
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
4938
+ `);
4939
+ }
4940
+ }
4941
+ process.stdout.write(`
4942
+ ${"\u2500".repeat(60)}
4943
+ `);
4944
+ process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
4945
+
4946
+ `);
4947
+ }
4948
+ async function traceLocal(receiptId) {
4949
+ const { readFileSync: readFileSync7, existsSync: existsSync5 } = await import("fs");
4950
+ const { join: join4 } = await import("path");
4951
+ const dir = process.cwd();
4952
+ const receiptsDir = join4(dir, ".protect-mcp", "receipts");
4953
+ if (!existsSync5(receiptsDir)) {
4954
+ process.stdout.write(` No local receipts found in ${receiptsDir}
4955
+
4956
+ `);
4957
+ return;
4958
+ }
4959
+ const { readdirSync } = await import("fs");
4960
+ const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
4961
+ process.stdout.write(` Scanning ${files.length} local receipts...
4962
+
4963
+ `);
4964
+ const receipts = [];
4965
+ for (const file of files) {
4966
+ try {
4967
+ const content = readFileSync7(join4(receiptsDir, file), "utf-8");
4968
+ const receipt = JSON.parse(content);
4969
+ receipts.push(receipt);
4970
+ } catch {
4971
+ }
4972
+ }
4973
+ const match = receipts.find(
4974
+ (r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
4975
+ );
4976
+ if (match) {
4977
+ const claims = match.signed_claims?.claims || match;
4978
+ process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
4979
+ `);
4980
+ process.stdout.write(` Event: ${claims.event_id || "?"}
4981
+ `);
4982
+ process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
4983
+ `);
4984
+ process.stdout.write(` Time: ${claims.event_time || "?"}
4985
+ `);
4986
+ if (claims.edges && claims.edges.length > 0) {
4987
+ process.stdout.write(`
4988
+ ${bold("Edges:")}
4989
+ `);
4990
+ for (const edge of claims.edges) {
4991
+ process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
4992
+ `);
4993
+ }
4994
+ }
4995
+ } else {
4996
+ process.stdout.write(` Receipt ${receiptId} not found locally.
4997
+ `);
4998
+ }
4999
+ process.stdout.write("\n");
5000
+ }
5001
+ function getTypeEmoji(type) {
5002
+ switch (type) {
5003
+ case "acta:observation":
5004
+ return "\u{1F441} ";
5005
+ case "acta:policy-load":
5006
+ return "\u{1F4CB}";
5007
+ case "acta:approval":
5008
+ return "\u2705";
5009
+ case "acta:decision":
5010
+ return "\u2696\uFE0F ";
5011
+ case "acta:execution":
5012
+ return "\u26A1";
5013
+ case "acta:outcome":
5014
+ return "\u{1F4E6}";
5015
+ case "acta:delegation":
5016
+ return "\u{1F91D}";
5017
+ case "acta:capability-attestation":
5018
+ return "\u{1F3C5}";
5019
+ default:
5020
+ return "\u{1F4C4}";
5021
+ }
5022
+ }
4383
5023
  async function main() {
4384
5024
  const args = process.argv.slice(2);
4385
5025
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
4386
5026
  printHelp();
4387
5027
  process.exit(0);
4388
5028
  }
5029
+ if (args[0] === "quickstart") {
5030
+ await handleQuickstart();
5031
+ return;
5032
+ }
4389
5033
  if (args[0] === "init") {
4390
5034
  await handleInit(args.slice(1));
4391
5035
  process.exit(0);
@@ -4410,6 +5054,18 @@ async function main() {
4410
5054
  await handleBundle(args.slice(1));
4411
5055
  process.exit(0);
4412
5056
  }
5057
+ if (args[0] === "trace") {
5058
+ await handleTrace(args.slice(1));
5059
+ process.exit(0);
5060
+ }
5061
+ if (args[0] === "simulate") {
5062
+ await handleSimulate(args.slice(1));
5063
+ process.exit(0);
5064
+ }
5065
+ if (args[0] === "report") {
5066
+ await handleReport(args.slice(1));
5067
+ process.exit(0);
5068
+ }
4413
5069
  const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
4414
5070
  let policy = null;
4415
5071
  let policyDigest = "none";
@@ -4460,6 +5116,85 @@ async function main() {
4460
5116
  const gateway = new ProtectGateway(config);
4461
5117
  await gateway.start();
4462
5118
  }
5119
+ async function handleSimulate(args) {
5120
+ let policyPath = "";
5121
+ let logPath = ".protect-mcp-log.jsonl";
5122
+ let tier = "unknown";
5123
+ let jsonOutput = false;
5124
+ for (let i = 0; i < args.length; i++) {
5125
+ if (args[i] === "--policy" && args[i + 1]) {
5126
+ policyPath = args[++i];
5127
+ } else if (args[i] === "--log" && args[i + 1]) {
5128
+ logPath = args[++i];
5129
+ } else if (args[i] === "--tier" && args[i + 1]) {
5130
+ tier = args[++i];
5131
+ } else if (args[i] === "--json") {
5132
+ jsonOutput = true;
5133
+ }
5134
+ }
5135
+ if (!policyPath) {
5136
+ process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
5137
+ process.exit(1);
5138
+ }
5139
+ const { existsSync: existsSync5 } = await import("fs");
5140
+ if (!existsSync5(logPath)) {
5141
+ process.stderr.write(`Log file not found: ${logPath}
5142
+ `);
5143
+ process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
5144
+ process.exit(1);
5145
+ }
5146
+ const { policy } = loadPolicy(policyPath);
5147
+ const entries = parseLogFile(logPath);
5148
+ if (entries.length === 0) {
5149
+ process.stderr.write("No tool call entries found in log file.\n");
5150
+ process.exit(1);
5151
+ }
5152
+ const summary = simulate(entries, policy, tier);
5153
+ summary.policy_file = policyPath;
5154
+ summary.log_file = logPath;
5155
+ if (jsonOutput) {
5156
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
5157
+ } else {
5158
+ process.stdout.write(formatSimulation(summary) + "\n");
5159
+ }
5160
+ }
5161
+ async function handleReport(args) {
5162
+ let period = 30;
5163
+ let format = "json";
5164
+ let outputPath = "";
5165
+ let dir = process.cwd();
5166
+ for (let i = 0; i < args.length; i++) {
5167
+ if (args[i] === "--period" && args[i + 1]) {
5168
+ const match = args[++i].match(/^(\d+)d$/);
5169
+ if (match) period = parseInt(match[1], 10);
5170
+ } else if (args[i] === "--format" && args[i + 1]) {
5171
+ format = args[++i];
5172
+ } else if (args[i] === "--output" && args[i + 1]) {
5173
+ outputPath = args[++i];
5174
+ } else if (args[i] === "--dir" && args[i + 1]) {
5175
+ dir = args[++i];
5176
+ }
5177
+ }
5178
+ const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
5179
+ const { join: join4 } = await import("path");
5180
+ const logPath = join4(dir, ".protect-mcp-log.jsonl");
5181
+ const receiptPath = join4(dir, ".protect-mcp-receipts.jsonl");
5182
+ const report = generateReport2(logPath, receiptPath, period);
5183
+ let output;
5184
+ if (format === "md") {
5185
+ output = formatReportMarkdown2(report);
5186
+ } else {
5187
+ output = JSON.stringify(report, null, 2);
5188
+ }
5189
+ if (outputPath) {
5190
+ const { writeFileSync: writeFileSync2 } = await import("fs");
5191
+ writeFileSync2(outputPath, output, "utf-8");
5192
+ process.stderr.write(`Report written to ${outputPath}
5193
+ `);
5194
+ } else {
5195
+ process.stdout.write(output + "\n");
5196
+ }
5197
+ }
4463
5198
  main().catch((err) => {
4464
5199
  process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
4465
5200
  `);