chainhint-mcp 1.3.2 → 1.3.3

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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Address evidence wording — MIRROR of the main repo's
3
+ * backend/supabase/functions/_shared/evidence-labels.ts (migration 129).
4
+ * Change both or neither; test/evidence.test.mjs pins the strings.
5
+ *
6
+ * Rule (owner decision 2026-09-14): "Sanctioned" is printed only for a
7
+ * sanctions designation. A law-enforcement attribution, an issuer freeze, a
8
+ * national seizure order and an industry blocklist each say what they are and
9
+ * that they are not a sanctions designation.
10
+ */
11
+ export declare const EVIDENCE_CLASSES: readonly ["sanctions_designation", "law_enforcement_attribution", "issuer_freeze", "national_seizure_order", "industry_blocklist", "analyst_verified"];
12
+ export type EvidenceClass = typeof EVIDENCE_CLASSES[number];
13
+ export interface EvidenceItem {
14
+ class: EvidenceClass | string;
15
+ authority: string;
16
+ subject?: string | null;
17
+ document_ref?: string | null;
18
+ document_url?: string | null;
19
+ document_date?: string | null;
20
+ legal_basis?: string | null;
21
+ token?: string | null;
22
+ chain?: string | null;
23
+ first_seen_at?: string | null;
24
+ }
25
+ export declare const NOT_A_SANCTIONS_DESIGNATION = "Not a sanctions designation.";
26
+ /** "⛔ Sanctioned — US OFAC · … " / "🧊 Frozen by issuer — Tether (USDT, TRON) · … Not a sanctions designation." */
27
+ export declare function evidenceText(e: EvidenceItem): string | null;
28
+ /** Designations first, then by strength of proof; unknown classes dropped. */
29
+ export declare function evidenceLines(items: ReadonlyArray<EvidenceItem> | null | undefined): string[];
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Address evidence wording — MIRROR of the main repo's
3
+ * backend/supabase/functions/_shared/evidence-labels.ts (migration 129).
4
+ * Change both or neither; test/evidence.test.mjs pins the strings.
5
+ *
6
+ * Rule (owner decision 2026-09-14): "Sanctioned" is printed only for a
7
+ * sanctions designation. A law-enforcement attribution, an issuer freeze, a
8
+ * national seizure order and an industry blocklist each say what they are and
9
+ * that they are not a sanctions designation.
10
+ */
11
+ export const EVIDENCE_CLASSES = [
12
+ "sanctions_designation",
13
+ "law_enforcement_attribution",
14
+ "issuer_freeze",
15
+ "national_seizure_order",
16
+ "industry_blocklist",
17
+ "analyst_verified",
18
+ ];
19
+ export const NOT_A_SANCTIONS_DESIGNATION = "Not a sanctions designation.";
20
+ const TITLES = {
21
+ sanctions_designation: { icon: "⛔", title: "Sanctioned" },
22
+ law_enforcement_attribution: { icon: "🚩", title: "Law-enforcement attribution" },
23
+ issuer_freeze: { icon: "🧊", title: "Frozen by issuer" },
24
+ national_seizure_order: { icon: "🏛️", title: "National seizure order" },
25
+ industry_blocklist: { icon: "⚠️", title: "Industry blocklist" },
26
+ analyst_verified: { icon: "🔎", title: "Analyst-verified" },
27
+ };
28
+ const CHAIN_NAMES = { bsc: "BSC", tron: "TRON", ton: "TON" };
29
+ function isEvidenceClass(v) {
30
+ return typeof v === "string" && EVIDENCE_CLASSES.includes(v);
31
+ }
32
+ function chainName(chain) {
33
+ const c = (chain ?? "").trim().toLowerCase();
34
+ if (!c)
35
+ return "";
36
+ return CHAIN_NAMES[c] ?? c.charAt(0).toUpperCase() + c.slice(1);
37
+ }
38
+ function day(value) {
39
+ if (!value)
40
+ return null;
41
+ const m = /^(\d{4}-\d{2}-\d{2})/.exec(value.trim());
42
+ return m ? m[1] : null;
43
+ }
44
+ const join = (parts, sep) => parts.filter((p) => !!p && p.trim() !== "").join(sep);
45
+ function attributionLine(e) {
46
+ const date = day(e.document_date);
47
+ let head = join([e.authority, e.document_ref], ", ");
48
+ if (date)
49
+ head = `${head} (${date})`;
50
+ return e.subject ? `${head}: ${e.subject}` : head;
51
+ }
52
+ /** "⛔ Sanctioned — US OFAC · … " / "🧊 Frozen by issuer — Tether (USDT, TRON) · … Not a sanctions designation." */
53
+ export function evidenceText(e) {
54
+ if (!isEvidenceClass(e.class))
55
+ return null;
56
+ const { icon, title } = TITLES[e.class];
57
+ const date = day(e.document_date);
58
+ let line;
59
+ switch (e.class) {
60
+ case "sanctions_designation":
61
+ line = join([e.authority, e.document_ref ?? e.legal_basis, date, e.subject], " · ");
62
+ break;
63
+ case "issuer_freeze": {
64
+ const what = join([e.token, chainName(e.chain)], ", ");
65
+ const when = date ?? join(["first seen by ChainHint", day(e.first_seen_at)], " ");
66
+ line = join([what ? `${e.authority} (${what})` : e.authority, when], " · ");
67
+ break;
68
+ }
69
+ case "law_enforcement_attribution":
70
+ case "national_seizure_order":
71
+ line = attributionLine(e);
72
+ break;
73
+ default:
74
+ line = join([e.authority, e.subject, date ?? (e.first_seen_at ? `first seen by ChainHint ${day(e.first_seen_at)}` : null)], " · ");
75
+ }
76
+ const caveat = e.class === "sanctions_designation" ? null : NOT_A_SANCTIONS_DESIGNATION;
77
+ return `${icon} ${caveat ? `${title} — ${line}. ${caveat}` : `${title} — ${line}`}`;
78
+ }
79
+ /** Designations first, then by strength of proof; unknown classes dropped. */
80
+ export function evidenceLines(items) {
81
+ if (!items?.length)
82
+ return [];
83
+ const rank = (c) => EVIDENCE_CLASSES.indexOf(c);
84
+ return [...items]
85
+ .filter((e) => isEvidenceClass(e.class))
86
+ .sort((a, b) => rank(a.class) - rank(b.class))
87
+ .map((e) => `- ${evidenceText(e)}`);
88
+ }
package/dist/index.js CHANGED
@@ -18,12 +18,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
19
  import { z } from "zod";
20
20
  import { apiErrorMessage, formatRiskLevel, lookupRiskLines, traceStatusLine } from "./verdicts.js";
21
+ import { evidenceLines } from "./evidence.js";
21
22
  // ── Config ────────────────────────────────────────────────────────────────────
22
23
  const API_KEY = process.env.CHAINHINT_API_KEY;
23
24
  const BASE_URL = process.env.CHAINHINT_API_URL ?? "https://kjiwfwymnuzxriokhcjk.supabase.co/functions/v1";
24
25
  const SUPABASE_URL = process.env.CHAINHINT_SUPABASE_URL ?? "https://kjiwfwymnuzxriokhcjk.supabase.co";
25
26
  const SUPABASE_ANON_KEY = process.env.CHAINHINT_SUPABASE_ANON_KEY ?? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtqaXdmd3ltbnV6eHJpb2toY2prIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI3MzkxODgsImV4cCI6MjA4ODMxNTE4OH0.VqzzF_jI8zF072cbjWEDbYo3PnMDlIPy621iWkXEqyo";
26
- const VERSION = "1.3.2";
27
+ const VERSION = "1.3.3";
27
28
  const USER_AGENT = `chainhint-mcp/${VERSION}`;
28
29
  const FREE_CHECKS_PER_DAY = 3;
29
30
  const UPGRADE_HINT = "set CHAINHINT_API_KEY (Agency plan, https://chainhint.com/pricing) for 10,000/day";
@@ -256,9 +257,14 @@ server.tool("check_wallet_risk", "Fast risk check for a crypto wallet address ag
256
257
  else {
257
258
  lines.push(`**Risk Score:** ${d.risk_score}/100 — **${(d.risk_level ?? formatRiskLevel(d.risk_score)).toUpperCase()}**`);
258
259
  }
259
- if (d.sanctions?.hit) {
260
- lines.push(`⛔ **SANCTIONED / OFAC-linked**`);
260
+ // sanctions.hit comes only from a sanctions designation (API since migration 129);
261
+ // the evidence lines say which authority and that other classes are not sanctions.
262
+ const walletEvidence = evidenceLines(d.evidence);
263
+ if (d.sanctions?.hit && !walletEvidence.some((l) => l.startsWith("- ⛔"))) {
264
+ lines.push(`⛔ **SANCTIONED**`);
261
265
  }
266
+ if (walletEvidence.length)
267
+ lines.push(`**Evidence:**`, ...walletEvidence);
262
268
  if (d.entity?.name) {
263
269
  const sub = d.entity.subcategory ? ` / ${d.entity.subcategory}` : "";
264
270
  const ver = d.entity.verified ? ", verified" : "";
@@ -328,6 +334,9 @@ server.tool("lookup_address", "Detailed lookup of a blockchain address: entity a
328
334
  lines.push(`**Labels:** ${[...new Set(d.labels)].join(", ")}`);
329
335
  // Never a score or "CLEAN" when there was nothing to score (verdicts.ts).
330
336
  lines.push(...lookupRiskLines(d));
337
+ const lookupEvidence = evidenceLines(d.evidence);
338
+ if (lookupEvidence.length)
339
+ lines.push(`**Evidence:**`, ...lookupEvidence);
331
340
  const sanctionIds = d.sanctions?.identifications ?? [];
332
341
  if (sanctionIds.length) {
333
342
  const names = sanctionIds.map((s) => [s.name, s.program].filter(Boolean).join(" / ")).filter(Boolean);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainhint-mcp",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "ChainHint MCP server \u2014 crypto risk intelligence tools for Claude Desktop and Cursor",
5
5
  "license": "MIT",
6
6
  "repository": {