chainhint-mcp 1.0.1 → 1.1.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/README.md +36 -15
- package/dist/index.d.ts +3 -2
- package/dist/index.js +80 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,16 +4,16 @@ Crypto risk intelligence for Claude Desktop, Cursor, and any MCP-compatible AI.
|
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
|
-
| Tool | Description |
|
|
8
|
-
|
|
9
|
-
| `check_wallet_risk` | Fast risk score, entity, labels, sanctions hit (54M+ labeled addresses)
|
|
10
|
-
| `lookup_address` | Deep address report — entity, risk factors, GoPlus flags, counterparty exposure, balance
|
|
11
|
-
| `get_trace_status` | Fund-trace summary for a public hack incident — hops, endpoints by type (exchange/mixer/bridge/defi), exposure
|
|
7
|
+
| Tool | Description | Limit |
|
|
8
|
+
|------|-------------|-------|
|
|
9
|
+
| `check_wallet_risk` | Fast risk score, entity, labels, sanctions hit (54M+ labeled addresses) | **3 / day** free · 10,000 / day with key |
|
|
10
|
+
| `lookup_address` | Deep address report — entity, risk factors, GoPlus flags, counterparty exposure, balance | **10 / day** free · 100 / hour with key |
|
|
11
|
+
| `get_trace_status` | Fund-trace summary for a public hack incident — hops, endpoints by type (exchange/mixer/bridge/defi), exposure | Unlimited |
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
15
15
|
- Node.js 18+
|
|
16
|
-
-
|
|
16
|
+
- Nothing else. **No API key needed to start** — every tool has a free daily allowance (per IP). An Agency plan key (`ch_live_...`, [chainhint.com/settings](https://chainhint.com/settings)) lifts the limits.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -38,10 +38,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
38
38
|
"mcpServers": {
|
|
39
39
|
"chainhint": {
|
|
40
40
|
"command": "npx",
|
|
41
|
-
"args": ["-y", "chainhint-mcp"]
|
|
42
|
-
"env": {
|
|
43
|
-
"CHAINHINT_API_KEY": "ch_live_your_key_here"
|
|
44
|
-
}
|
|
41
|
+
"args": ["-y", "chainhint-mcp"]
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
}
|
|
@@ -56,15 +53,39 @@ Add to `.cursor/mcp.json` in your project (or global `~/.cursor/mcp.json`):
|
|
|
56
53
|
"mcpServers": {
|
|
57
54
|
"chainhint": {
|
|
58
55
|
"command": "npx",
|
|
59
|
-
"args": ["-y", "chainhint-mcp"]
|
|
60
|
-
"env": {
|
|
61
|
-
"CHAINHINT_API_KEY": "ch_live_your_key_here"
|
|
62
|
-
}
|
|
56
|
+
"args": ["-y", "chainhint-mcp"]
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
}
|
|
66
60
|
```
|
|
67
61
|
|
|
62
|
+
## Claude Code
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
claude mcp add chainhint -- npx -y chainhint-mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Lifting the free limits
|
|
69
|
+
|
|
70
|
+
Add an Agency key to the server entry (any client):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"env": { "CHAINHINT_API_KEY": "ch_live_your_key_here" }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What the agent sees
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
## Wallet Risk Report: 0x47666f...9486e2
|
|
80
|
+
**Chain:** ethereum
|
|
81
|
+
**Risk Score:** 85/100 — **CRITICAL**
|
|
82
|
+
**Entity:** Bybit Hack Exploiter (hacker)
|
|
83
|
+
**Labels:** Hacker/Exploiter, Bybit Hack Exploiter
|
|
84
|
+
**In ChainHint DB:** yes (sources: chainhint:manual)
|
|
85
|
+
|
|
86
|
+
Free tier: 2 of 3 checks left today — set CHAINHINT_API_KEY (Agency plan, https://chainhint.com/pricing) for 10,000/day.
|
|
87
|
+
```
|
|
88
|
+
|
|
68
89
|
## Development (no build step)
|
|
69
90
|
|
|
70
91
|
```bash
|
|
@@ -75,7 +96,7 @@ npm run dev
|
|
|
75
96
|
|
|
76
97
|
| Variable | Required | Description |
|
|
77
98
|
|----------|----------|-------------|
|
|
78
|
-
| `CHAINHINT_API_KEY` | — |
|
|
99
|
+
| `CHAINHINT_API_KEY` | — | Agency plan key from chainhint.com. Lifts the free limits (3 checks + 10 lookups per day) to 10,000 / day |
|
|
79
100
|
| `CHAINHINT_API_URL` | — | Override API base URL (default: production) |
|
|
80
101
|
| `CHAINHINT_SUPABASE_ANON_KEY` | — | Override anon key for get_trace_status (public incidents) |
|
|
81
102
|
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - lookup_address → address-lookup API (entity, risk factors, exposure, balance) — public
|
|
11
11
|
* - get_trace_status → public incident fund-trace summary (endpoints, hops, exposure) — public
|
|
12
12
|
*
|
|
13
|
-
* Auth:
|
|
14
|
-
*
|
|
13
|
+
* Auth: works without a key. check_wallet_risk has a free tier of 3 checks
|
|
14
|
+
* per IP per day; lookup_address 10 per day; get_trace_status is unlimited.
|
|
15
|
+
* CHAINHINT_API_KEY (Agency plan, ch_live_...) lifts the limits to 10,000/day.
|
|
15
16
|
*/
|
|
16
17
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* - lookup_address → address-lookup API (entity, risk factors, exposure, balance) — public
|
|
11
11
|
* - get_trace_status → public incident fund-trace summary (endpoints, hops, exposure) — public
|
|
12
12
|
*
|
|
13
|
-
* Auth:
|
|
14
|
-
*
|
|
13
|
+
* Auth: works without a key. check_wallet_risk has a free tier of 3 checks
|
|
14
|
+
* per IP per day; lookup_address 10 per day; get_trace_status is unlimited.
|
|
15
|
+
* CHAINHINT_API_KEY (Agency plan, ch_live_...) lifts the limits to 10,000/day.
|
|
15
16
|
*/
|
|
16
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -21,30 +22,55 @@ const API_KEY = process.env.CHAINHINT_API_KEY;
|
|
|
21
22
|
const BASE_URL = process.env.CHAINHINT_API_URL ?? "https://kjiwfwymnuzxriokhcjk.supabase.co/functions/v1";
|
|
22
23
|
const SUPABASE_URL = process.env.CHAINHINT_SUPABASE_URL ?? "https://kjiwfwymnuzxriokhcjk.supabase.co";
|
|
23
24
|
const SUPABASE_ANON_KEY = process.env.CHAINHINT_SUPABASE_ANON_KEY ?? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtqaXdmd3ltbnV6eHJpb2toY2prIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI3MzkxODgsImV4cCI6MjA4ODMxNTE4OH0.VqzzF_jI8zF072cbjWEDbYo3PnMDlIPy621iWkXEqyo";
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const VERSION = "1.1.0";
|
|
26
|
+
const USER_AGENT = `chainhint-mcp/${VERSION}`;
|
|
27
|
+
const FREE_CHECKS_PER_DAY = 3;
|
|
28
|
+
const UPGRADE_HINT = "set CHAINHINT_API_KEY (Agency plan, https://chainhint.com/pricing) for 10,000/day";
|
|
26
29
|
if (!API_KEY) {
|
|
27
|
-
console.error(
|
|
28
|
-
console.error(" lookup_address and get_trace_status work without a key.");
|
|
30
|
+
console.error(`[chainhint-mcp] No CHAINHINT_API_KEY — running on the free tier (${FREE_CHECKS_PER_DAY} wallet checks + 10 lookups per day per IP).`);
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
async function apiGet(path, params, opts) {
|
|
32
|
+
async function apiGet(path, params) {
|
|
32
33
|
const url = new URL(`${BASE_URL}${path}`);
|
|
33
34
|
for (const [k, v] of Object.entries(params)) {
|
|
34
35
|
if (v)
|
|
35
36
|
url.searchParams.set(k, v);
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// User-Agent lets the backend attribute traffic (api_usage_log.caller) and
|
|
39
|
+
// apply the MCP-specific free allowance. The key, when present, goes in
|
|
40
|
+
// X-Api-Key so both wallet-reputation and address-lookup recognise it.
|
|
41
|
+
const headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"User-Agent": USER_AGENT,
|
|
44
|
+
};
|
|
45
|
+
if (API_KEY) {
|
|
39
46
|
headers["X-Api-Key"] = API_KEY;
|
|
40
47
|
headers["Authorization"] = `Bearer ${API_KEY}`;
|
|
41
48
|
}
|
|
42
49
|
const res = await fetch(url.toString(), { headers });
|
|
43
50
|
const body = (await res.json().catch(() => ({ error: `HTTP ${res.status}` })));
|
|
44
51
|
if (!res.ok) {
|
|
45
|
-
|
|
52
|
+
let msg = body?.error ?? `HTTP ${res.status}: ${url.toString()}`;
|
|
53
|
+
if (res.status === 429 && body?.reset_at) {
|
|
54
|
+
msg += ` Resets at ${new Date(body.reset_at * 1000).toISOString()}.`;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(msg);
|
|
46
57
|
}
|
|
47
|
-
return body;
|
|
58
|
+
return { body, headers: res.headers };
|
|
59
|
+
}
|
|
60
|
+
/** "Free tier: 2 of 3 checks left today — set CHAINHINT_API_KEY …" or null. */
|
|
61
|
+
function quotaLine(headers, what) {
|
|
62
|
+
if (headers.get("x-chainhint-tier") !== "free")
|
|
63
|
+
return null;
|
|
64
|
+
const limit = Number(headers.get("x-ratelimit-limit"));
|
|
65
|
+
const remaining = Number(headers.get("x-ratelimit-remaining"));
|
|
66
|
+
if (!Number.isFinite(limit) || !Number.isFinite(remaining))
|
|
67
|
+
return null;
|
|
68
|
+
// Bypass-listed IPs get MAX_SAFE_INTEGER from the backend — no quota to report.
|
|
69
|
+
if (limit >= 1_000_000)
|
|
70
|
+
return null;
|
|
71
|
+
const reset = Number(headers.get("x-ratelimit-reset"));
|
|
72
|
+
const resetStr = Number.isFinite(reset) && reset > 0 ? ` (resets ${new Date(reset * 1000).toISOString().slice(0, 16)}Z)` : "";
|
|
73
|
+
return `Free tier: ${remaining} of ${limit} ${what} left today${resetStr} — ${UPGRADE_HINT}.`;
|
|
48
74
|
}
|
|
49
75
|
async function supabaseGet(table, params) {
|
|
50
76
|
const url = new URL(`${SUPABASE_URL}/rest/v1/${table}`);
|
|
@@ -106,31 +132,58 @@ function formatExposure(label, buckets) {
|
|
|
106
132
|
}),
|
|
107
133
|
];
|
|
108
134
|
}
|
|
135
|
+
/** Public incident whose attacker_address matches, or null (errors swallowed — best effort). */
|
|
136
|
+
async function findPublicIncidentByAttacker(address) {
|
|
137
|
+
try {
|
|
138
|
+
const rows = await supabaseGet("public_incidents_view", {
|
|
139
|
+
select: "id,title,chain,amount_usd,estimated_loss_usd,risk_score",
|
|
140
|
+
attacker_address: `eq.${normalizeAddr(address)}`,
|
|
141
|
+
limit: "1",
|
|
142
|
+
});
|
|
143
|
+
return rows[0] ?? null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
109
149
|
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
110
150
|
const server = new McpServer({
|
|
111
151
|
name: "chainhint",
|
|
112
|
-
version:
|
|
152
|
+
version: VERSION,
|
|
113
153
|
});
|
|
114
154
|
// ── Tool 1: check_wallet_risk ─────────────────────────────────────────────────
|
|
115
|
-
server.tool("check_wallet_risk", "Fast risk check for a crypto wallet address against ChainHint's 54M+ labeled address database (12 chains). Returns risk score 0-100, risk level (clean/low/medium/high/critical/sanctioned), entity name and category, labels, and sanctions hit.
|
|
155
|
+
server.tool("check_wallet_risk", "Fast risk check for a crypto wallet address against ChainHint's 54M+ labeled address database (12 chains). Returns risk score 0-100, risk level (clean/low/medium/high/critical/sanctioned), entity name and category, labels, and sanctions hit. Use it to decide allow/warn/block before paying or interacting with a counterparty wallet. Free: 3 checks per day without a key; CHAINHINT_API_KEY (Agency plan) lifts it to 10,000/day. For a deeper report (risk factors, exposure, balance) use lookup_address.", {
|
|
116
156
|
address: z.string().describe("Wallet address to check (EVM 0x..., Bitcoin, or Solana)"),
|
|
117
157
|
chain: z.string().optional().describe("Blockchain: ethereum, bsc, polygon, arbitrum, optimism, base, avalanche, solana, bitcoin (default: auto-detect from address format)"),
|
|
118
158
|
}, async ({ address, chain }) => {
|
|
119
|
-
if (!API_KEY) {
|
|
120
|
-
return { content: [{ type: "text", text: NO_KEY_MESSAGE }] };
|
|
121
|
-
}
|
|
122
159
|
try {
|
|
123
160
|
const params = { address };
|
|
124
161
|
if (chain)
|
|
125
162
|
params.chain = chain;
|
|
126
163
|
// wallet-reputation returns a flat object (no {success,data} wrapper).
|
|
127
|
-
const d = await apiGet("/wallet-reputation", params
|
|
164
|
+
const { body: d, headers } = await apiGet("/wallet-reputation", params);
|
|
128
165
|
const lines = [
|
|
129
166
|
`## Wallet Risk Report: ${truncateAddr(d.address)}`,
|
|
130
167
|
`**Address:** ${d.address}`,
|
|
131
168
|
`**Chain:** ${d.chain}`,
|
|
132
|
-
`**Risk Score:** ${d.risk_score}/100 — **${(d.risk_level ?? formatRiskLevel(d.risk_score)).toUpperCase()}**`,
|
|
133
169
|
];
|
|
170
|
+
// Canon (same as chainhint.com and the TG bot): an address that is not in
|
|
171
|
+
// the labeled database has NO DATA — that is not evidence it is clean.
|
|
172
|
+
// The API still returns risk_score 0 / "clean" for not-found, so the
|
|
173
|
+
// wording is fixed here, and public incidents are cross-checked so a
|
|
174
|
+
// known hack attacker that never got an `addresses` row is not
|
|
175
|
+
// presented as unknown.
|
|
176
|
+
if (!d.found_in_db) {
|
|
177
|
+
lines.push(`**Risk:** ⚪ NO DATA — address is not in ChainHint's labeled database. This is not evidence it is clean.`);
|
|
178
|
+
const inc = await findPublicIncidentByAttacker(d.address);
|
|
179
|
+
if (inc) {
|
|
180
|
+
lines.push(`⚠️ **Known attacker in a public hack incident:** ${inc.title ?? "Unnamed incident"} (${inc.chain}, loss ${usd(inc.amount_usd ?? inc.estimated_loss_usd)}${inc.risk_score != null ? `, incident risk ${inc.risk_score}/100` : ""}). Treat as HIGH risk.`, `🔗 https://chainhint.com/incident/${inc.id}`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(`Use lookup_address for an on-chain assessment (risk factors, GoPlus flags, counterparty exposure).`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push(`**Risk Score:** ${d.risk_score}/100 — **${(d.risk_level ?? formatRiskLevel(d.risk_score)).toUpperCase()}**`);
|
|
186
|
+
}
|
|
134
187
|
if (d.sanctions?.hit) {
|
|
135
188
|
lines.push(`⛔ **SANCTIONED / OFAC-linked**`);
|
|
136
189
|
}
|
|
@@ -151,6 +204,9 @@ server.tool("check_wallet_risk", "Fast risk check for a crypto wallet address ag
|
|
|
151
204
|
lines.push(`**Type:** ${d.is_contract ? "Smart Contract" : "EOA (wallet)"}`);
|
|
152
205
|
lines.push(`**In ChainHint DB:** ${d.found_in_db ? "yes" : "no"}${d.sources?.length ? ` (sources: ${d.sources.join(", ")})` : ""}`);
|
|
153
206
|
lines.push(`**Checked at:** ${d.checked_at}`);
|
|
207
|
+
const quota = quotaLine(headers, "checks");
|
|
208
|
+
if (quota)
|
|
209
|
+
lines.push(``, quota);
|
|
154
210
|
lines.push(`\n*Powered by ChainHint — chainhint.com*`);
|
|
155
211
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
156
212
|
}
|
|
@@ -159,7 +215,7 @@ server.tool("check_wallet_risk", "Fast risk check for a crypto wallet address ag
|
|
|
159
215
|
}
|
|
160
216
|
});
|
|
161
217
|
// ── Tool 2: lookup_address ────────────────────────────────────────────────────
|
|
162
|
-
server.tool("lookup_address", "Detailed lookup of a blockchain address: entity attribution, risk score with the factors behind it, sanctions and GoPlus security flags, counterparty exposure (where funds came from / went to, by category with named entities), balance, token count and transaction count.
|
|
218
|
+
server.tool("lookup_address", "Detailed lookup of a blockchain address: entity attribution, risk score with the factors behind it, sanctions and GoPlus security flags, counterparty exposure (where funds came from / went to, by category with named entities), balance, token count and transaction count. Free: 10 lookups per day without a key; CHAINHINT_API_KEY lifts it. Supports EVM chains, Bitcoin, Solana, TRON, TON.", {
|
|
163
219
|
address: z.string().describe("Blockchain address to look up"),
|
|
164
220
|
chain: z.string().optional().describe("Blockchain (ethereum, bsc, polygon, arbitrum, optimism, base, avalanche, solana, bitcoin, tron, ton)"),
|
|
165
221
|
}, async ({ address, chain }) => {
|
|
@@ -167,7 +223,7 @@ server.tool("lookup_address", "Detailed lookup of a blockchain address: entity a
|
|
|
167
223
|
const params = { address };
|
|
168
224
|
if (chain)
|
|
169
225
|
params.chain = chain;
|
|
170
|
-
const data = await apiGet("/address-lookup", params
|
|
226
|
+
const { body: data, headers: lookupHeaders } = await apiGet("/address-lookup", params);
|
|
171
227
|
if (!data.success || !data.data) {
|
|
172
228
|
return { content: [{ type: "text", text: `Error: ${data.error ?? "Unknown error"}` }] };
|
|
173
229
|
}
|
|
@@ -223,6 +279,9 @@ server.tool("lookup_address", "Detailed lookup of a blockchain address: entity a
|
|
|
223
279
|
lines.push(...formatExposure("Inflow by category", ex.inflow));
|
|
224
280
|
lines.push(...formatExposure("Outflow by category", ex.outflow));
|
|
225
281
|
}
|
|
282
|
+
const lookupQuota = quotaLine(lookupHeaders, "lookups");
|
|
283
|
+
if (lookupQuota)
|
|
284
|
+
lines.push(``, lookupQuota);
|
|
226
285
|
lines.push(``, `🔗 https://chainhint.com/address/${d.address}?chain=${d.chain}`);
|
|
227
286
|
lines.push(`*Powered by ChainHint — chainhint.com*`);
|
|
228
287
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|