chainhint-mcp 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChainHint
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # ChainHint MCP Server
2
+
3
+ Crypto risk intelligence for Claude Desktop, Cursor, and any MCP-compatible AI.
4
+
5
+ ## Tools
6
+
7
+ | Tool | Description |
8
+ |------|-------------|
9
+ | `check_wallet_risk` | Risk score, entity label, sanctions check for any wallet |
10
+ | `lookup_address` | Full address details — entity, category, tx history |
11
+ | `get_trace_status` | Fund trace status for hack incidents — where did the money go? |
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 18+
16
+ - ChainHint Agency plan API key (`ch_live_...`) from [chainhint.com/settings](https://chainhint.com/settings)
17
+
18
+ ## Install
19
+
20
+ No install needed — run straight from npm with `npx` (see configs below).
21
+
22
+ From source instead:
23
+
24
+ ```bash
25
+ git clone https://github.com/seomarlboro/chainhint-mcp
26
+ cd chainhint-mcp
27
+ npm install
28
+ npm run build
29
+ # then use "command": "node", "args": ["/absolute/path/to/chainhint-mcp/dist/index.js"]
30
+ ```
31
+
32
+ ## Claude Desktop
33
+
34
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "chainhint": {
40
+ "command": "npx",
41
+ "args": ["-y", "chainhint-mcp"],
42
+ "env": {
43
+ "CHAINHINT_API_KEY": "ch_live_your_key_here"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Cursor
51
+
52
+ Add to `.cursor/mcp.json` in your project (or global `~/.cursor/mcp.json`):
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "chainhint": {
58
+ "command": "npx",
59
+ "args": ["-y", "chainhint-mcp"],
60
+ "env": {
61
+ "CHAINHINT_API_KEY": "ch_live_your_key_here"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Development (no build step)
69
+
70
+ ```bash
71
+ npm run dev
72
+ ```
73
+
74
+ ## Environment Variables
75
+
76
+ | Variable | Required | Description |
77
+ |----------|----------|-------------|
78
+ | `CHAINHINT_API_KEY` | ✅ | API key from chainhint.com (Agency plan) |
79
+ | `CHAINHINT_API_URL` | — | Override API base URL (default: production) |
80
+ | `CHAINHINT_SUPABASE_ANON_KEY` | — | For get_trace_status (public incidents) |
81
+
82
+ ## Example prompts
83
+
84
+ > "Check if 0x47666fab8bd0ac7003bce3f5c3585383f09486e2 is a known hacker"
85
+
86
+ > "Look up the Bybit attacker address on Ethereum"
87
+
88
+ > "What's the trace status for attacker 0x47666fab..."
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ChainHint MCP Server
4
+ *
5
+ * Provides crypto risk intelligence tools to Claude Desktop, Cursor,
6
+ * and any MCP-compatible AI client.
7
+ *
8
+ * Tools:
9
+ * - check_wallet_risk → wallet-reputation API (risk score, labels, sanctions)
10
+ * - lookup_address → address-lookup API (entity, category, transaction history)
11
+ * - get_trace_status → incident fund trace status (flow graph summary)
12
+ *
13
+ * Auth: set CHAINHINT_API_KEY env var (Agency plan API key: ch_live_...)
14
+ */
15
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ChainHint MCP Server
4
+ *
5
+ * Provides crypto risk intelligence tools to Claude Desktop, Cursor,
6
+ * and any MCP-compatible AI client.
7
+ *
8
+ * Tools:
9
+ * - check_wallet_risk → wallet-reputation API (risk score, labels, sanctions)
10
+ * - lookup_address → address-lookup API (entity, category, transaction history)
11
+ * - get_trace_status → incident fund trace status (flow graph summary)
12
+ *
13
+ * Auth: set CHAINHINT_API_KEY env var (Agency plan API key: ch_live_...)
14
+ */
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { z } from "zod";
18
+ // ── Config ────────────────────────────────────────────────────────────────────
19
+ const API_KEY = process.env.CHAINHINT_API_KEY;
20
+ const BASE_URL = process.env.CHAINHINT_API_URL ?? "https://kjiwfwymnuzxriokhcjk.supabase.co/functions/v1";
21
+ const SUPABASE_URL = process.env.CHAINHINT_SUPABASE_URL ?? "https://kjiwfwymnuzxriokhcjk.supabase.co";
22
+ const SUPABASE_ANON_KEY = process.env.CHAINHINT_SUPABASE_ANON_KEY ?? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtqaXdmd3ltbnV6eHJpb2toY2prIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI3MzkxODgsImV4cCI6MjA4ODMxNTE4OH0.VqzzF_jI8zF072cbjWEDbYo3PnMDlIPy621iWkXEqyo";
23
+ if (!API_KEY) {
24
+ console.error("[chainhint-mcp] ERROR: CHAINHINT_API_KEY is not set.");
25
+ console.error(" Get your API key from chainhint.com → Settings → API Keys (Agency plan required)");
26
+ process.exit(1);
27
+ }
28
+ // ── HTTP helpers ──────────────────────────────────────────────────────────────
29
+ async function apiGet(path, params = {}) {
30
+ const url = new URL(`${BASE_URL}${path}`);
31
+ for (const [k, v] of Object.entries(params)) {
32
+ if (v)
33
+ url.searchParams.set(k, v);
34
+ }
35
+ const res = await fetch(url.toString(), {
36
+ headers: {
37
+ "X-Api-Key": API_KEY,
38
+ "Authorization": `Bearer ${API_KEY}`,
39
+ "Content-Type": "application/json",
40
+ },
41
+ });
42
+ const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
43
+ if (!res.ok) {
44
+ throw new Error(body?.error ?? `HTTP ${res.status}: ${url.toString()}`);
45
+ }
46
+ return body;
47
+ }
48
+ async function supabaseGet(table, params) {
49
+ const url = new URL(`${SUPABASE_URL}/rest/v1/${table}`);
50
+ for (const [k, v] of Object.entries(params)) {
51
+ url.searchParams.set(k, v);
52
+ }
53
+ const res = await fetch(url.toString(), {
54
+ headers: {
55
+ "apikey": SUPABASE_ANON_KEY,
56
+ "Authorization": `Bearer ${SUPABASE_ANON_KEY}`,
57
+ "X-Api-Key": API_KEY,
58
+ "Accept": "application/json",
59
+ },
60
+ });
61
+ if (!res.ok) {
62
+ const err = await res.text();
63
+ throw new Error(`Supabase ${res.status}: ${err}`);
64
+ }
65
+ return res.json();
66
+ }
67
+ // ── Format helpers ────────────────────────────────────────────────────────────
68
+ function formatRiskLevel(score) {
69
+ if (score >= 90)
70
+ return "CRITICAL";
71
+ if (score >= 70)
72
+ return "HIGH";
73
+ if (score >= 40)
74
+ return "MEDIUM";
75
+ if (score >= 10)
76
+ return "LOW";
77
+ return "CLEAN";
78
+ }
79
+ function truncateAddr(addr) {
80
+ return addr.length > 12 ? `${addr.slice(0, 8)}...${addr.slice(-6)}` : addr;
81
+ }
82
+ // ── MCP Server ────────────────────────────────────────────────────────────────
83
+ const server = new McpServer({
84
+ name: "chainhint",
85
+ version: "1.0.0",
86
+ });
87
+ // ── Tool 1: check_wallet_risk ─────────────────────────────────────────────────
88
+ server.tool("check_wallet_risk", "Check the risk score and labels for a crypto wallet address. Returns risk level (CLEAN/LOW/MEDIUM/HIGH/CRITICAL), entity label, category, sanctions status, and transaction statistics. Use this to assess whether a wallet is associated with hacks, scams, mixers, or sanctioned entities.", {
89
+ address: z.string().describe("Wallet address to check (EVM 0x..., Bitcoin, or Solana)"),
90
+ chain: z.string().optional().describe("Blockchain: ethereum, bsc, polygon, arbitrum, optimism, base, avalanche, solana, bitcoin (default: auto-detect from address format)"),
91
+ }, async ({ address, chain }) => {
92
+ try {
93
+ const params = { address };
94
+ if (chain)
95
+ params.chain = chain;
96
+ const data = await apiGet("/wallet-reputation", params);
97
+ if (!data.success || !data.data) {
98
+ return { content: [{ type: "text", text: `Error: ${data.error ?? "Unknown error"}` }] };
99
+ }
100
+ const d = data.data;
101
+ const risk = d.risk;
102
+ const entity = d.entity;
103
+ const sanctions = d.sanctions;
104
+ const stats = d.stats;
105
+ const lines = [
106
+ `## Wallet Risk Report: ${truncateAddr(d.address)}`,
107
+ `**Chain:** ${d.chain}`,
108
+ `**Risk Score:** ${risk?.score ?? "N/A"}/100 — **${risk?.level ?? formatRiskLevel(risk?.score ?? 0)}**`,
109
+ ];
110
+ if (entity?.name) {
111
+ lines.push(`**Entity:** ${entity.name} (${entity.category})`);
112
+ if (entity.label)
113
+ lines.push(`**Label:** ${entity.label}`);
114
+ }
115
+ else {
116
+ lines.push(`**Entity:** Unknown / unlabeled`);
117
+ }
118
+ if (sanctions?.is_sanctioned) {
119
+ lines.push(`⛔ **SANCTIONED** — Programs: ${sanctions.programs.join(", ")}`);
120
+ }
121
+ if (risk?.flags?.length) {
122
+ lines.push(`**Risk Flags:** ${risk.flags.join(", ")}`);
123
+ }
124
+ if (stats) {
125
+ lines.push(`**Transactions:** ${stats.tx_count?.toLocaleString() ?? "N/A"}`);
126
+ if (stats.first_seen)
127
+ lines.push(`**First seen:** ${stats.first_seen.slice(0, 10)}`);
128
+ if (stats.last_seen)
129
+ lines.push(`**Last seen:** ${stats.last_seen.slice(0, 10)}`);
130
+ }
131
+ lines.push(`\n*Powered by ChainHint — chainhint.com*`);
132
+ return { content: [{ type: "text", text: lines.join("\n") }] };
133
+ }
134
+ catch (err) {
135
+ return { content: [{ type: "text", text: `Error checking wallet: ${err.message}` }] };
136
+ }
137
+ });
138
+ // ── Tool 2: lookup_address ────────────────────────────────────────────────────
139
+ server.tool("lookup_address", "Look up detailed information about a blockchain address including entity label, risk assessment, transaction history, and known associations. More detailed than check_wallet_risk — includes transaction counts, token holdings summary, and entity metadata.", {
140
+ address: z.string().describe("Blockchain address to look up"),
141
+ chain: z.string().optional().describe("Blockchain (ethereum, bsc, polygon, arbitrum, optimism, base, avalanche, solana, bitcoin)"),
142
+ }, async ({ address, chain }) => {
143
+ try {
144
+ const params = { address };
145
+ if (chain)
146
+ params.chain = chain;
147
+ const data = await apiGet("/address-lookup", params);
148
+ if (!data.success || !data.data) {
149
+ return { content: [{ type: "text", text: `Error: ${data.error ?? "Unknown error"}` }] };
150
+ }
151
+ const d = data.data;
152
+ const lines = [
153
+ `## Address Lookup: ${truncateAddr(d.address)}`,
154
+ `**Chain:** ${d.chain}`,
155
+ `**Type:** ${d.is_contract ? "Smart Contract" : "EOA (wallet)"}`,
156
+ ];
157
+ if (d.entity)
158
+ lines.push(`**Entity:** ${d.entity}`);
159
+ if (d.label)
160
+ lines.push(`**Label:** ${d.label}`);
161
+ if (d.category)
162
+ lines.push(`**Category:** ${d.category}`);
163
+ if (d.balance)
164
+ lines.push(`**Balance:** ${d.balance}`);
165
+ const score = d.risk?.score;
166
+ if (score !== undefined) {
167
+ lines.push(`**Risk Score:** ${score}/100 — ${formatRiskLevel(score)}`);
168
+ }
169
+ if (d.risk?.flags?.length) {
170
+ lines.push(`**Flags:** ${d.risk.flags.join(", ")}`);
171
+ }
172
+ if (d.sanctions?.is_sanctioned) {
173
+ lines.push(`⛔ **SANCTIONED**`);
174
+ }
175
+ if (d.tx_count)
176
+ lines.push(`**Transactions:** ${d.tx_count.toLocaleString()}`);
177
+ if (d.first_seen)
178
+ lines.push(`**First seen:** ${d.first_seen.slice(0, 10)}`);
179
+ if (d.last_seen)
180
+ lines.push(`**Last seen:** ${d.last_seen.slice(0, 10)}`);
181
+ lines.push(`\n*Powered by ChainHint — chainhint.com*`);
182
+ return { content: [{ type: "text", text: lines.join("\n") }] };
183
+ }
184
+ catch (err) {
185
+ return { content: [{ type: "text", text: `Error looking up address: ${err.message}` }] };
186
+ }
187
+ });
188
+ // ── Tool 3: get_trace_status ──────────────────────────────────────────────────
189
+ server.tool("get_trace_status", "Get the fund trace status for a crypto hack incident. Returns how stolen funds moved — number of hops, total amount traced, known endpoints (exchanges, mixers, bridges), and current movement status (in_transit, mixing, reached_exchange, dormant). Useful for incident response and understanding where stolen funds went.", {
190
+ attacker_address: z.string().optional().describe("Attacker wallet address to look up incident by"),
191
+ incident_id: z.string().optional().describe("Incident UUID (from chainhint.com) — alternative to attacker_address"),
192
+ }, async ({ attacker_address, incident_id }) => {
193
+ try {
194
+ if (!attacker_address && !incident_id) {
195
+ return { content: [{ type: "text", text: "Error: provide either attacker_address or incident_id" }] };
196
+ }
197
+ // Query public incidents via Supabase REST
198
+ let queryParams = {
199
+ select: "id,title,status,chain,attacker_address,amount_usd,estimated_loss_usd,created_at,source",
200
+ is_public: "eq.true",
201
+ limit: "1",
202
+ order: "created_at.desc",
203
+ };
204
+ if (incident_id) {
205
+ queryParams["id"] = `eq.${incident_id}`;
206
+ delete queryParams["is_public"];
207
+ }
208
+ else if (attacker_address) {
209
+ queryParams["attacker_address"] = `eq.${attacker_address.toLowerCase()}`;
210
+ }
211
+ const rows = await supabaseGet("incidents", queryParams);
212
+ if (!rows?.length) {
213
+ return {
214
+ content: [{
215
+ type: "text",
216
+ text: attacker_address
217
+ ? `No public incident found for attacker address ${attacker_address}.\nCheck https://chainhint.com/velocity for tracked incidents.`
218
+ : `Incident ${incident_id} not found or not public.`,
219
+ }],
220
+ };
221
+ }
222
+ const inc = rows[0];
223
+ const lossUsd = inc.amount_usd ?? inc.estimated_loss_usd;
224
+ const lines = [
225
+ `## Fund Trace: ${inc.title ?? "Unnamed Incident"}`,
226
+ `**Incident ID:** ${inc.id}`,
227
+ `**Chain:** ${inc.chain}`,
228
+ `**Status:** ${inc.status.toUpperCase()}`,
229
+ `**Attacker:** ${truncateAddr(inc.attacker_address)}`,
230
+ `**Loss:** ${lossUsd ? `$${(lossUsd / 1_000_000).toFixed(2)}M` : "Unknown"}`,
231
+ `**Date:** ${inc.created_at.slice(0, 10)}`,
232
+ ];
233
+ if (inc.status.toLowerCase() === "traced") {
234
+ lines.push(`\n✅ Trace complete — view full flow graph and counterparty exposure on ChainHint.`);
235
+ }
236
+ else if (inc.status.toLowerCase() === "analyzing") {
237
+ lines.push(`\n⏳ Trace in progress...`);
238
+ }
239
+ else {
240
+ lines.push(`\n⚠️ Status: ${inc.status}`);
241
+ }
242
+ lines.push(`\n🔗 View full trace: https://chainhint.com/incident/${inc.id}`);
243
+ lines.push(`*Powered by ChainHint — chainhint.com*`);
244
+ return { content: [{ type: "text", text: lines.join("\n") }] };
245
+ }
246
+ catch (err) {
247
+ return { content: [{ type: "text", text: `Error fetching trace: ${err.message}` }] };
248
+ }
249
+ });
250
+ // ── Start ─────────────────────────────────────────────────────────────────────
251
+ const transport = new StdioServerTransport();
252
+ await server.connect(transport);
253
+ console.error("[chainhint-mcp] Server running on stdio");
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "chainhint-mcp",
3
+ "version": "1.0.0",
4
+ "description": "ChainHint MCP server — crypto risk intelligence tools for Claude Desktop and Cursor",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/seomarlboro/chainhint-mcp.git"
9
+ },
10
+ "homepage": "https://chainhint.com/docs/api",
11
+ "main": "dist/index.js",
12
+ "type": "module",
13
+ "bin": {
14
+ "chainhint-mcp": "dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "crypto",
26
+ "blockchain",
27
+ "wallet",
28
+ "risk",
29
+ "aml",
30
+ "sanctions",
31
+ "forensics",
32
+ "claude"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "dev": "tsx src/index.ts",
37
+ "start": "node dist/index.js",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.0.0",
42
+ "zod": "^4.3.6"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "tsx": "^4.0.0",
47
+ "typescript": "^5.0.0"
48
+ }
49
+ }