@fidacy/mcp 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -118,3 +118,9 @@ semantic versioning.
118
118
  - MCP handshake now reports the real package version (was hardcoded "0.1.0"
119
119
  through 0.1.12 — same class as the client_version telemetry bug, same fix:
120
120
  injected from package.json at build time).
121
+
122
+ ## 0.1.14 — 2026-07-03
123
+
124
+ - Granular block-type telemetry: DENY reasons now split into lookalike (BEC),
125
+ duplicate-invoice, payee, cap, window and invalid, so the public threat page
126
+ can count blocks by attack class. Still zero PII (an enum, never the rule text).
package/dist/core.js CHANGED
@@ -423,7 +423,7 @@ function resolveMandateRules(cfg) {
423
423
  }
424
424
 
425
425
  // src/telemetry.ts
426
- var CLIENT_VERSION = true ? "0.1.13" : "dev";
426
+ var CLIENT_VERSION = true ? "0.1.15" : "dev";
427
427
  var currentShell = "mcp";
428
428
  function telemetryEnabled() {
429
429
  const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
@@ -444,8 +444,13 @@ var flushTimer = null;
444
444
  function resultOf(status, violatedRule) {
445
445
  if (status === "ALLOW") return "allow";
446
446
  const r = violatedRule ?? "";
447
- if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
447
+ if (r.startsWith("payee_lookalike")) return "deny_lookalike";
448
448
  if (r.startsWith("payee_not_in_allowlist")) return "deny_payee";
449
+ if (r.startsWith("duplicate_invoice")) return "deny_duplicate";
450
+ if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
451
+ if (r.startsWith("mandate_revoked") || r.startsWith("before_mandate") || r.startsWith("after_mandate"))
452
+ return "deny_window";
453
+ if (r.startsWith("non_positive_amount") || r.startsWith("invalid_")) return "deny_invalid";
449
454
  return "deny_scope";
450
455
  }
451
456
  function record(type, result) {
package/dist/index.js CHANGED
@@ -452,7 +452,7 @@ function resolveMandateRules(cfg) {
452
452
  }
453
453
 
454
454
  // src/telemetry.ts
455
- var CLIENT_VERSION = true ? "0.1.13" : "dev";
455
+ var CLIENT_VERSION = true ? "0.1.15" : "dev";
456
456
  var currentShell = "mcp";
457
457
  function telemetryEnabled() {
458
458
  const v = (process.env.FIDACY_DISABLE_TELEMETRY ?? "").trim().toLowerCase();
@@ -473,8 +473,13 @@ var flushTimer = null;
473
473
  function resultOf(status, violatedRule) {
474
474
  if (status === "ALLOW") return "allow";
475
475
  const r = violatedRule ?? "";
476
- if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
476
+ if (r.startsWith("payee_lookalike")) return "deny_lookalike";
477
477
  if (r.startsWith("payee_not_in_allowlist")) return "deny_payee";
478
+ if (r.startsWith("duplicate_invoice")) return "deny_duplicate";
479
+ if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
480
+ if (r.startsWith("mandate_revoked") || r.startsWith("before_mandate") || r.startsWith("after_mandate"))
481
+ return "deny_window";
482
+ if (r.startsWith("non_positive_amount") || r.startsWith("invalid_")) return "deny_invalid";
478
483
  return "deny_scope";
479
484
  }
480
485
  function record(type, result) {
@@ -675,8 +680,72 @@ async function postOnce(fetchImpl, url, headers, payload, timeoutMs) {
675
680
  return parsed;
676
681
  }
677
682
 
683
+ // src/artifacts.ts
684
+ import { createHash } from "node:crypto";
685
+ import { createReadStream } from "node:fs";
686
+ import { pipeline } from "node:stream/promises";
687
+ var ARTIFACT_KINDS = [
688
+ "contract",
689
+ "invoice",
690
+ "prescription",
691
+ "claim",
692
+ "document",
693
+ "image",
694
+ "audio",
695
+ "video",
696
+ "custom"
697
+ ];
698
+ async function hashFile(path) {
699
+ const hash = createHash("sha256");
700
+ await pipeline(createReadStream(path), async function* (source) {
701
+ for await (const chunk of source) {
702
+ hash.update(chunk);
703
+ yield;
704
+ }
705
+ });
706
+ return hash.digest("hex");
707
+ }
708
+ async function engineFetch(method, pathAndQuery, cfg, body) {
709
+ const base = requireSafeBaseUrl(cfg.engineUrl);
710
+ const controller = new AbortController();
711
+ const timer = setTimeout(() => controller.abort(), 1e4);
712
+ let res;
713
+ try {
714
+ res = await fetch(`${base}${pathAndQuery}`, {
715
+ method,
716
+ headers: {
717
+ authorization: `Bearer ${cfg.apiKey}`,
718
+ ...body !== void 0 ? { "content-type": "application/json" } : {}
719
+ },
720
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
721
+ signal: controller.signal
722
+ });
723
+ } catch {
724
+ throw new AssessError({ type: "network_error", status: 0 });
725
+ } finally {
726
+ clearTimeout(timer);
727
+ }
728
+ let json;
729
+ try {
730
+ json = await res.json();
731
+ } catch {
732
+ throw new AssessError({ type: "bad_response", status: res.status });
733
+ }
734
+ if (!res.ok) {
735
+ const type = typeof json?.error === "string" ? String(json.error) : "engine_error";
736
+ throw new AssessError({ type, status: res.status });
737
+ }
738
+ return json;
739
+ }
740
+ async function anchorArtifact(params, cfg) {
741
+ return await engineFetch("POST", "/v1/artifacts", cfg, params);
742
+ }
743
+ async function findArtifacts(sha2562, cfg) {
744
+ return await engineFetch("GET", `/v1/artifacts?sha256=${sha2562}`, cfg);
745
+ }
746
+
678
747
  // src/provision.ts
679
- var CLIENT_VERSION2 = true ? "0.1.13" : "dev";
748
+ var CLIENT_VERSION2 = true ? "0.1.15" : "dev";
680
749
  function provisionEnabled() {
681
750
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
682
751
  return !(v === "1" || v === "true" || v === "yes");
@@ -776,7 +845,7 @@ function weekOneBootNudge(upgradeToolName) {
776
845
  var state = ensureState();
777
846
  var core = makeCore();
778
847
  var subject = process.env.FIDACY_SUBJECT ?? "agent:demo";
779
- var SERVER_VERSION = true ? "0.1.13" : "dev";
848
+ var SERVER_VERSION = true ? "0.1.15" : "dev";
780
849
  var server = new McpServer({ name: "fidacy", version: SERVER_VERSION });
781
850
  server.registerTool(
782
851
  "request_payment",
@@ -896,6 +965,112 @@ server.registerTool(
896
965
  }
897
966
  }
898
967
  );
968
+ server.registerTool(
969
+ "anchor_artifact",
970
+ {
971
+ title: "Anchor Artifact (Bitcoin-anchored integrity proof)",
972
+ description: "Prove an artifact existed exactly as-is at this moment, and make any later tampering detectable. Give a file `path` (hashed locally with SHA-256 \u2014 the file itself is NEVER uploaded) or a precomputed `sha256`. The hash is registered on Fidacy's tamper-evident audit chain, which is checkpointed to the Bitcoin blockchain, and you get a signed receipt (JWS) verifiable offline against the engine JWKS. Use it for contracts, invoices, medical prescriptions, insurance claims, images, audio, video. `kind` defaults to document; optional `label` is a short reference (an invoice number, a case id \u2014 no PII).",
973
+ inputSchema: {
974
+ path: z.string().optional(),
975
+ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
976
+ kind: z.enum(ARTIFACT_KINDS).optional(),
977
+ label: z.string().max(120).optional(),
978
+ subject: z.string().max(120).optional()
979
+ },
980
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
981
+ },
982
+ async ({ path, sha256: sha2562, kind, label, subject: subj }) => {
983
+ const engineUrl = process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com";
984
+ const apiKey = (process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
985
+ if (!apiKey) {
986
+ return {
987
+ content: [{ type: "text", text: "anchor_artifact needs an engine key \u2014 free in about a minute: call the upgrade tool, or set FIDACY_ENGINE_API_KEY if you already have one." }],
988
+ isError: true
989
+ };
990
+ }
991
+ if (!path && !sha2562) {
992
+ return { content: [{ type: "text", text: "Give a file `path` (hashed locally) or a precomputed `sha256`." }], isError: true };
993
+ }
994
+ let hash = sha2562 ?? "";
995
+ if (!hash && path) {
996
+ try {
997
+ hash = await hashFile(path);
998
+ } catch {
999
+ return { content: [{ type: "text", text: "Could not read that file locally (check the path and permissions). Nothing was sent anywhere." }], isError: true };
1000
+ }
1001
+ }
1002
+ try {
1003
+ const r = await anchorArtifact(
1004
+ { sha256: hash, kind: kind ?? "document", ...label ? { label } : {}, ...subj ? { subject: subj } : {} },
1005
+ { engineUrl, apiKey }
1006
+ );
1007
+ return {
1008
+ content: [{
1009
+ type: "text",
1010
+ text: `ANCHORED ${r.kind} \xB7 sha256 ${hash.slice(0, 16)}\u2026 \xB7 audit seq ${r.audit.seq} \xB7 Bitcoin checkpoint: ${r.anchor.status}. The file never left this machine; only its hash was registered. Keep the receipt (structured output) \u2014 it verifies offline and proves this artifact existed exactly as-is, signed, at ${r.ts}.`
1011
+ }],
1012
+ structuredContent: r
1013
+ };
1014
+ } catch (e) {
1015
+ if (e instanceof AssessError) {
1016
+ return { content: [{ type: "text", text: `ANCHOR ${e.status}: ${e.type}` }], isError: true };
1017
+ }
1018
+ return { content: [{ type: "text", text: "ANCHOR failed: unexpected error" }], isError: true };
1019
+ }
1020
+ }
1021
+ );
1022
+ server.registerTool(
1023
+ "check_artifact",
1024
+ {
1025
+ title: "Check Artifact (was this hash anchored?)",
1026
+ description: "Check whether an artifact was anchored by this account and the state of its Bitcoin checkpoint. Give a file `path` (hashed locally, never uploaded) or a `sha256`. If the current hash of a file does NOT match any anchored record that you expected to match, the file changed since anchoring \u2014 that is exactly the tampering signal.",
1027
+ inputSchema: {
1028
+ path: z.string().optional(),
1029
+ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional()
1030
+ },
1031
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1032
+ },
1033
+ async ({ path, sha256: sha2562 }) => {
1034
+ const engineUrl = process.env.FIDACY_ENGINE_URL ?? "https://api.fidacy.com";
1035
+ const apiKey = (process.env.FIDACY_ENGINE_API_KEY ?? "").trim();
1036
+ if (!apiKey) {
1037
+ return {
1038
+ content: [{ type: "text", text: "check_artifact needs an engine key \u2014 free in about a minute: call the upgrade tool, or set FIDACY_ENGINE_API_KEY if you already have one." }],
1039
+ isError: true
1040
+ };
1041
+ }
1042
+ if (!path && !sha2562) {
1043
+ return { content: [{ type: "text", text: "Give a file `path` (hashed locally) or a `sha256`." }], isError: true };
1044
+ }
1045
+ let hash = sha2562 ?? "";
1046
+ if (!hash && path) {
1047
+ try {
1048
+ hash = await hashFile(path);
1049
+ } catch {
1050
+ return { content: [{ type: "text", text: "Could not read that file locally (check the path and permissions). Nothing was sent anywhere." }], isError: true };
1051
+ }
1052
+ }
1053
+ try {
1054
+ const r = await findArtifacts(hash, { engineUrl, apiKey });
1055
+ if (!r.artifacts.length) {
1056
+ return {
1057
+ content: [{ type: "text", text: `NOT FOUND \xB7 sha256 ${hash.slice(0, 16)}\u2026 has no anchored record in this account. If you expected a match, the file has changed since it was anchored.` }],
1058
+ structuredContent: { sha256: hash, artifacts: [] }
1059
+ };
1060
+ }
1061
+ const first = r.artifacts[0];
1062
+ return {
1063
+ content: [{ type: "text", text: `FOUND ${r.artifacts.length} record(s) \xB7 newest: ${String(first.kind)} anchored ${String(first.createdAt)} (audit seq ${String(first.auditSeq)}). The current content matches the anchored hash byte for byte.` }],
1064
+ structuredContent: { sha256: hash, ...r }
1065
+ };
1066
+ } catch (e) {
1067
+ if (e instanceof AssessError) {
1068
+ return { content: [{ type: "text", text: `CHECK ${e.status}: ${e.type}` }], isError: true };
1069
+ }
1070
+ return { content: [{ type: "text", text: "CHECK failed: unexpected error" }], isError: true };
1071
+ }
1072
+ }
1073
+ );
899
1074
  server.registerTool(
900
1075
  "upgrade",
901
1076
  {
package/dist/lib.js CHANGED
@@ -571,7 +571,7 @@ function resolveMandateRules(cfg) {
571
571
  }
572
572
 
573
573
  // src/telemetry.ts
574
- var CLIENT_VERSION = true ? "0.1.13" : "dev";
574
+ var CLIENT_VERSION = true ? "0.1.15" : "dev";
575
575
  var currentShell = "mcp";
576
576
  function setTelemetryShell(shell) {
577
577
  currentShell = shell;
@@ -595,8 +595,13 @@ var flushTimer = null;
595
595
  function resultOf(status, violatedRule) {
596
596
  if (status === "ALLOW") return "allow";
597
597
  const r = violatedRule ?? "";
598
- if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
598
+ if (r.startsWith("payee_lookalike")) return "deny_lookalike";
599
599
  if (r.startsWith("payee_not_in_allowlist")) return "deny_payee";
600
+ if (r.startsWith("duplicate_invoice")) return "deny_duplicate";
601
+ if (r.startsWith("per_tx_cap") || r.startsWith("total_cap")) return "deny_cap";
602
+ if (r.startsWith("mandate_revoked") || r.startsWith("before_mandate") || r.startsWith("after_mandate"))
603
+ return "deny_window";
604
+ if (r.startsWith("non_positive_amount") || r.startsWith("invalid_")) return "deny_invalid";
600
605
  return "deny_scope";
601
606
  }
602
607
  function record(type, result) {
@@ -694,7 +699,7 @@ function requestUpgrade() {
694
699
  }
695
700
 
696
701
  // src/provision.ts
697
- var CLIENT_VERSION2 = true ? "0.1.13" : "dev";
702
+ var CLIENT_VERSION2 = true ? "0.1.15" : "dev";
698
703
  function provisionEnabled() {
699
704
  const v = (process.env.FIDACY_DISABLE_PROVISION ?? "").trim().toLowerCase();
700
705
  return !(v === "1" || v === "true" || v === "yes");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidacy/mcp",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Fidacy action firewall for AI agents. Mandate-gated payment authorization as an MCP server.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://fidacy.com",
@@ -36,6 +36,12 @@
36
36
  "publishConfig": {
37
37
  "access": "public"
38
38
  },
39
+ "scripts": {
40
+ "build": "node scripts/bundle.mjs",
41
+ "dev": "tsx watch src/index.ts",
42
+ "start": "node dist/index.js",
43
+ "prepublishOnly": "npm run build"
44
+ },
39
45
  "engines": {
40
46
  "node": ">=18"
41
47
  },
@@ -44,19 +50,14 @@
44
50
  "zod": "^3.25.0"
45
51
  },
46
52
  "devDependencies": {
53
+ "@fidacy/firewall": "workspace:*",
47
54
  "@types/node": "^22.10.0",
48
55
  "tsx": "^4.19.2",
49
- "typescript": "^5.7.2",
50
- "@fidacy/firewall": "0.1.0"
56
+ "typescript": "^5.7.2"
51
57
  },
52
58
  "mcpName": "com.fidacy/mcp",
53
59
  "repository": {
54
60
  "type": "git",
55
61
  "url": "git+https://github.com/lucaslubi/fidacy-mcp.git"
56
- },
57
- "scripts": {
58
- "build": "node scripts/bundle.mjs",
59
- "dev": "tsx watch src/index.ts",
60
- "start": "node dist/index.js"
61
62
  }
62
63
  }