@quantakrypto/mcp 0.4.4 → 0.5.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/tools.js CHANGED
@@ -824,6 +824,58 @@ const checkDependencyTool = {
824
824
  };
825
825
  },
826
826
  };
827
+ /**
828
+ * JSON-schema shape of one finding from `scan_path --format json` (mirrors the
829
+ * core {@link Finding} type). Used as the `items` schema for every findings
830
+ * array below so MCP clients/inspectors get a described element shape instead of
831
+ * a bare `type: "array"`. Extra finding fields are allowed (no
832
+ * additionalProperties:false), only ruleId + location.file are required — which
833
+ * is exactly what {@link areFindings} enforces.
834
+ */
835
+ const FINDING_ITEM_SCHEMA = {
836
+ type: "object",
837
+ description: "A single finding from `scan_path --format json`.",
838
+ properties: {
839
+ ruleId: { type: "string", description: 'Stable rule id, e.g. "rsa-keygen".' },
840
+ title: { type: "string" },
841
+ category: { type: "string" },
842
+ severity: { type: "string", description: "critical | high | medium | low | info." },
843
+ confidence: { type: "string" },
844
+ algorithm: { type: "string", description: "Classical algorithm family, when applicable." },
845
+ hndl: { type: "boolean", description: "Exposed to harvest-now-decrypt-later." },
846
+ message: { type: "string" },
847
+ remediation: { type: "string" },
848
+ cwe: { type: "string", description: 'e.g. "CWE-327".' },
849
+ location: {
850
+ type: "object",
851
+ description: "Where the finding is.",
852
+ properties: {
853
+ file: { type: "string" },
854
+ line: { type: "number" },
855
+ },
856
+ required: ["file"],
857
+ },
858
+ },
859
+ required: ["ruleId", "location"],
860
+ };
861
+ /** JSON-schema shape of one triage verdict for `apply_triage`. */
862
+ const VERDICT_ITEM_SCHEMA = {
863
+ type: "object",
864
+ description: "A triage verdict for one finding.",
865
+ properties: {
866
+ fingerprint: {
867
+ type: "string",
868
+ description: "Fingerprint of the finding this verdict applies to.",
869
+ },
870
+ exposureScore: {
871
+ type: "number",
872
+ description: "Real-world exposure (higher = more exposed).",
873
+ },
874
+ priority: { type: "string", enum: ["now", "soon", "later"] },
875
+ rationale: { type: "string", description: "Why this exposure score / priority." },
876
+ },
877
+ required: ["fingerprint", "exposureScore", "priority", "rationale"],
878
+ };
827
879
  const scoreDeltaTool = {
828
880
  name: "score_delta",
829
881
  description: "Compute the readiness-score and HNDL change between two finding sets (e.g. before " +
@@ -834,9 +886,14 @@ const scoreDeltaTool = {
834
886
  properties: {
835
887
  before: {
836
888
  type: "array",
889
+ items: FINDING_ITEM_SCHEMA,
837
890
  description: "Findings before the change (from a scan's JSON findings).",
838
891
  },
839
- after: { type: "array", description: "Findings after the change." },
892
+ after: {
893
+ type: "array",
894
+ items: FINDING_ITEM_SCHEMA,
895
+ description: "Findings after the change.",
896
+ },
840
897
  },
841
898
  required: ["before", "after"],
842
899
  additionalProperties: false,
@@ -918,7 +975,11 @@ const triageFindingsTool = {
918
975
  inputSchema: {
919
976
  type: "object",
920
977
  properties: {
921
- findings: { type: "array", description: "Findings from a scan's JSON output." },
978
+ findings: {
979
+ type: "array",
980
+ items: FINDING_ITEM_SCHEMA,
981
+ description: "Findings from a scan's JSON output.",
982
+ },
922
983
  },
923
984
  required: ["findings"],
924
985
  additionalProperties: false,
@@ -988,8 +1049,16 @@ const applyTriageTool = {
988
1049
  inputSchema: {
989
1050
  type: "object",
990
1051
  properties: {
991
- findings: { type: "array", description: "The findings that were triaged." },
992
- verdicts: { type: "array", description: "One verdict per finding, keyed by fingerprint." },
1052
+ findings: {
1053
+ type: "array",
1054
+ items: FINDING_ITEM_SCHEMA,
1055
+ description: "The findings that were triaged.",
1056
+ },
1057
+ verdicts: {
1058
+ type: "array",
1059
+ items: VERDICT_ITEM_SCHEMA,
1060
+ description: "One verdict per finding, keyed by fingerprint.",
1061
+ },
993
1062
  },
994
1063
  required: ["findings", "verdicts"],
995
1064
  additionalProperties: false,
@@ -1053,7 +1122,11 @@ const remediateFindingsTool = {
1053
1122
  inputSchema: {
1054
1123
  type: "object",
1055
1124
  properties: {
1056
- findings: { type: "array", description: "Findings from a scan's JSON output." },
1125
+ findings: {
1126
+ type: "array",
1127
+ items: FINDING_ITEM_SCHEMA,
1128
+ description: "Findings from a scan's JSON output.",
1129
+ },
1057
1130
  },
1058
1131
  required: ["findings"],
1059
1132
  additionalProperties: false,
@@ -1142,6 +1215,100 @@ const applyVerifiedPatchTool = {
1142
1215
  }, null, 2));
1143
1216
  },
1144
1217
  };
1218
+ /**
1219
+ * `probe_endpoint` — actively probe ONE live TLS/SSH endpoint the caller OWNS for
1220
+ * post-quantum readiness. This is the ONLY MCP tool that opens a network socket;
1221
+ * the qprobe plane is loaded via dynamic import so the server stays offline until
1222
+ * the tool is actually invoked, and the ownership attestation gate is enforced in
1223
+ * qProbe's `runProbe` before any connection. On the HTTP transport it is OFF by
1224
+ * default (see {@link NETWORK_TOOL_NAMES}) — a hosted endpoint should not probe
1225
+ * arbitrary hosts — but a trusted operator can opt in with
1226
+ * QUANTAKRYPTO_MCP_ALLOW_NETWORK=1.
1227
+ */
1228
+ const probeEndpointTool = {
1229
+ name: "probe_endpoint",
1230
+ description: "Actively probe ONE live TLS/SSH endpoint YOU OWN for post-quantum readiness " +
1231
+ "(PQC-hybrid key exchange X25519MLKEM768, classical certificate posture). REQUIRES " +
1232
+ "an ownership attestation: set i_own_this=true to confirm you are authorized to test " +
1233
+ "the target. Refuses CIDR ranges / wildcards / lists — one host at a time. Performs " +
1234
+ "only a benign, unauthenticated handshake and never modifies the endpoint. NOTE: this " +
1235
+ "is the ONLY quantakrypto MCP tool that opens a network connection; the server is " +
1236
+ "otherwise offline. Over HTTP it is disabled unless the operator sets " +
1237
+ "QUANTAKRYPTO_MCP_ALLOW_NETWORK=1.",
1238
+ inputSchema: {
1239
+ type: "object",
1240
+ properties: {
1241
+ target: {
1242
+ type: "string",
1243
+ description: "A single host or host:port you own (no ranges/CIDRs/wildcards).",
1244
+ },
1245
+ mode: {
1246
+ type: "string",
1247
+ enum: ["tls", "ssh", "auto"],
1248
+ description: "Probe mode (default: auto — SSH on :22, TLS otherwise).",
1249
+ },
1250
+ i_own_this: {
1251
+ type: "boolean",
1252
+ description: "Attestation that you are authorized to probe this endpoint. Must be true; the probe is refused otherwise.",
1253
+ },
1254
+ timeout_ms: { type: "number", description: "Per-connection timeout in ms (default 8000)." },
1255
+ },
1256
+ required: ["target", "i_own_this"],
1257
+ additionalProperties: false,
1258
+ },
1259
+ async handler(args) {
1260
+ const target = args.target;
1261
+ if (typeof target !== "string" || target.length === 0) {
1262
+ return errorResult("probe_endpoint requires a non-empty 'target' string.");
1263
+ }
1264
+ if (args.i_own_this !== true) {
1265
+ return errorResult("probe_endpoint refused: set i_own_this=true to attest you are authorized to probe this endpoint. Active probing of endpoints you do not own may be unlawful.");
1266
+ }
1267
+ const mode = args.mode === "tls" || args.mode === "ssh" ? args.mode : "auto";
1268
+ const timeoutMs = typeof args.timeout_ms === "number" ? args.timeout_ms : undefined;
1269
+ // Dynamic import keeps the networked qprobe plane out of the server process
1270
+ // until this tool is actually used; runProbe enforces the attestation gate.
1271
+ const qprobe = await import("@quantakrypto/qprobe");
1272
+ // Parse the target explicitly so a CIDR/range/list refusal returns a helpful
1273
+ // message rather than a scrubbed internal error.
1274
+ let parsed;
1275
+ try {
1276
+ parsed = qprobe.parseTarget(target, mode === "ssh" ? 22 : 443);
1277
+ }
1278
+ catch (e) {
1279
+ return errorResult(`probe_endpoint: ${e instanceof Error ? e.message : String(e)}`);
1280
+ }
1281
+ const probed = await safe("probe_endpoint", () => qprobe.runProbe({ targets: [parsed], mode, attest: { iOwnThis: true }, timeoutMs }));
1282
+ if (!probed.ok)
1283
+ return probed.result;
1284
+ const { reports, findings, inventory } = probed.value;
1285
+ const lines = [];
1286
+ let unreachable = 0;
1287
+ for (const r of reports) {
1288
+ lines.push(`${r.target.host}:${r.target.port} [${r.mode}]`);
1289
+ const err = r.ssh?.error ?? r.tls?.error ?? r.hybrid?.error;
1290
+ if (err && r.findings.length === 0) {
1291
+ unreachable++;
1292
+ // Do NOT let an unreachable/errored endpoint read as a clean 100/100.
1293
+ lines.push(` ⚠ probe error: ${err} — endpoint NOT assessed (not a clean result)`);
1294
+ }
1295
+ for (const p of r.positives)
1296
+ lines.push(` ✓ ${p}`);
1297
+ for (const f of r.findings)
1298
+ lines.push(` [${f.severity}] ${f.title} — ${f.message}`);
1299
+ }
1300
+ const note = unreachable > 0
1301
+ ? " — NOTE: an endpoint could not be reached/handshaken, so this is NOT a clean bill of health"
1302
+ : "";
1303
+ lines.push(`\n${findings.length} finding${findings.length === 1 ? "" : "s"} · ${inventory.hndlCount} HNDL-exposed · readiness ${inventory.readinessScore}/100${note}`);
1304
+ return {
1305
+ content: [
1306
+ { type: "text", text: lines.join("\n") },
1307
+ { type: "text", text: JSON.stringify({ findings, inventory }, null, 2) },
1308
+ ],
1309
+ };
1310
+ },
1311
+ };
1145
1312
  /**
1146
1313
  * Tools that read arbitrary filesystem paths. Disabled by default on the HTTP
1147
1314
  * transport (see {@link ./http.ts}) because a hosted endpoint must not be an
@@ -1154,6 +1321,13 @@ export const FS_TOOL_NAMES = [
1154
1321
  "generate_cbom",
1155
1322
  "plan_migration",
1156
1323
  ];
1324
+ /**
1325
+ * Tools that open network connections. On the HTTP transport they are OFF by
1326
+ * default — a hosted MCP should not probe arbitrary hosts — but a trusted operator
1327
+ * can opt in with QUANTAKRYPTO_MCP_ALLOW_NETWORK=1 (mirroring the FS-tools opt-in).
1328
+ * Always available on the local stdio transport, which trusts the local user.
1329
+ */
1330
+ export const NETWORK_TOOL_NAMES = ["probe_endpoint"];
1157
1331
  /** All quantakrypto MCP tools, in a stable order. */
1158
1332
  export const quantakryptoTools = [
1159
1333
  scanPathTool,
@@ -1173,6 +1347,8 @@ export const quantakryptoTools = [
1173
1347
  applyTriageTool,
1174
1348
  remediateFindingsTool,
1175
1349
  applyVerifiedPatchTool,
1350
+ // The only networked tool — offline until invoked; refused on the HTTP transport.
1351
+ probeEndpointTool,
1176
1352
  ];
1177
1353
  /** The core version these tools are built against (re-exported for diagnostics). */
1178
1354
  export const CORE_VERSION = VERSION;