keryx-mcp 0.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 +74 -0
- package/dist/keryx-mcp.mjs +184 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Keryx MCP server — add Keryx to any agent
|
|
2
|
+
|
|
3
|
+
Give any MCP-capable agent (Claude Code, Claude Desktop, Cursor, …) a tool that asks **Keryx** a
|
|
4
|
+
question. Keryx autonomously buys paid sources under a budget, answers with inline citations, and
|
|
5
|
+
**pays every creator it cites** in USDC on Arc. The toll for each call is paid from *your own*
|
|
6
|
+
Arc-testnet wallet — so every call is a real on-chain payment, visible live on
|
|
7
|
+
[keryx.cc/dashboard](https://keryx.cc/dashboard).
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
|------|--------------|
|
|
13
|
+
| `ask_keryx` | Ask a research question (+ optional USDC `budget`). Returns a cited answer and the creators Keryx paid downstream. Costs **0.02 USDC** per call, paid from your wallet. |
|
|
14
|
+
| `keryx_wallet_status` | Show the wallet this server pays from — address, balances, whether it's ready, and how to fund it. **Run this first.** |
|
|
15
|
+
|
|
16
|
+
## Setup (≈3 minutes)
|
|
17
|
+
|
|
18
|
+
Published to npm as [`keryx-mcp`](https://www.npmjs.com/package/keryx-mcp) — no clone, no build.
|
|
19
|
+
|
|
20
|
+
### Add it to Claude Code (one line)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
claude mcp add keryx -- npx -y keryx-mcp@latest
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Or add it to any MCP client (Claude Desktop, Cursor, Windsurf, …)
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"keryx": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "keryx-mcp@latest"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Fund the wallet, then ask
|
|
40
|
+
|
|
41
|
+
1. Call **`keryx_wallet_status`** — it prints the wallet address it pays from.
|
|
42
|
+
2. Open the [Circle faucet](https://faucet.circle.com), pick **Arc Testnet**, paste that address.
|
|
43
|
+
(20 USDC / 2h — also covers gas.)
|
|
44
|
+
3. Call **`ask_keryx`** with your question. The server deposits to Circle Gateway on the first call
|
|
45
|
+
and pays the toll; Keryx researches, answers with citations, and pays the creators it cited.
|
|
46
|
+
|
|
47
|
+
That's it — the answer comes back with the on-chain payment proof and a link to the live dashboard
|
|
48
|
+
where your call now appears as external traction.
|
|
49
|
+
|
|
50
|
+
## Configuration (env)
|
|
51
|
+
|
|
52
|
+
All optional — sane Arc-testnet defaults are built in.
|
|
53
|
+
|
|
54
|
+
| Var | Default | Purpose |
|
|
55
|
+
|-----|---------|---------|
|
|
56
|
+
| `KERYX_BASE_URL` | `https://keryx.cc` | Keryx deployment to call. |
|
|
57
|
+
| `KERYX_BUYER_PRIVATE_KEY` | *(generated)* | Bring your own funded Arc wallet instead of the generated one. |
|
|
58
|
+
| `KERYX_WALLET_FILE` | `~/.keryx/buyer-wallet.json` | Where the generated wallet is persisted. |
|
|
59
|
+
| `KERYX_GATEWAY_DEPOSIT` | `0.5` | USDC moved into Gateway per top-up. |
|
|
60
|
+
| `KERYX_RPC_URL` | `https://rpc.testnet.arc.network` | Arc testnet RPC. |
|
|
61
|
+
|
|
62
|
+
> **Real money, testnet.** Calls settle real USDC on Arc testnet. The generated wallet holds only
|
|
63
|
+
> what you faucet into it; Keryx never touches your keys. To go mainnet, point `KERYX_BASE_URL` at a
|
|
64
|
+
> mainnet deployment and fund with real USDC — only with eyes open.
|
|
65
|
+
|
|
66
|
+
## From source (development)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/tang-vu/keryx && cd keryx && npm install
|
|
70
|
+
claude mcp add keryx -- node --import tsx --no-warnings "$(pwd)/mcp/keryx-mcp-server.mts"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`npm run build` (in `mcp/`) bundles `keryx-mcp-server.mts` → `dist/keryx-mcp.mjs` with esbuild; that
|
|
74
|
+
single file is what ships to npm and runs under plain `node`.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// keryx-mcp-server.mts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// keryx-buyer.mts
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { GatewayClient } from "@circle-fin/x402-batching/client";
|
|
13
|
+
import { createPublicClient, erc20Abi, formatUnits, http, parseUnits } from "viem";
|
|
14
|
+
import { arcTestnet } from "viem/chains";
|
|
15
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
16
|
+
var USDC = process.env.KERYX_USDC_ADDRESS ?? "0x3600000000000000000000000000000000000000";
|
|
17
|
+
var RPC = process.env.KERYX_RPC_URL ?? "https://rpc.testnet.arc.network";
|
|
18
|
+
var CHAIN = process.env.KERYX_NETWORK ?? "arcTestnet";
|
|
19
|
+
var BASE_URL = (process.env.KERYX_BASE_URL ?? "https://keryx.cc").replace(/\/$/, "");
|
|
20
|
+
var FEE_USDC = Number(process.env.KERYX_A2A_FEE ?? "0.02");
|
|
21
|
+
var DEPOSIT_USDC = process.env.KERYX_GATEWAY_DEPOSIT ?? "0.5";
|
|
22
|
+
var FAUCET = "https://faucet.circle.com";
|
|
23
|
+
var EXPLORER = "https://testnet.arcscan.app";
|
|
24
|
+
var WALLET_FILE = process.env.KERYX_WALLET_FILE ?? path.join(os.homedir(), ".keryx", "buyer-wallet.json");
|
|
25
|
+
function loadOrCreateKey() {
|
|
26
|
+
const envKey = process.env.KERYX_BUYER_PRIVATE_KEY;
|
|
27
|
+
if (envKey && envKey.startsWith("0x")) return envKey;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(fs.readFileSync(WALLET_FILE, "utf8")).privateKey;
|
|
30
|
+
} catch {
|
|
31
|
+
const key2 = generatePrivateKey();
|
|
32
|
+
fs.mkdirSync(path.dirname(WALLET_FILE), { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
WALLET_FILE,
|
|
35
|
+
JSON.stringify({ privateKey: key2, address: privateKeyToAccount(key2).address }, null, 2)
|
|
36
|
+
);
|
|
37
|
+
return key2;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var key = loadOrCreateKey();
|
|
41
|
+
var account = privateKeyToAccount(key);
|
|
42
|
+
var gateway = new GatewayClient({ chain: CHAIN, privateKey: key, rpcUrl: RPC });
|
|
43
|
+
var pub = createPublicClient({ chain: arcTestnet, transport: http(RPC) });
|
|
44
|
+
var meta = {
|
|
45
|
+
address: account.address,
|
|
46
|
+
baseUrl: BASE_URL,
|
|
47
|
+
feeUsdc: FEE_USDC,
|
|
48
|
+
faucet: FAUCET,
|
|
49
|
+
explorer: EXPLORER,
|
|
50
|
+
walletFile: WALLET_FILE
|
|
51
|
+
};
|
|
52
|
+
var fee = parseUnits(String(FEE_USDC), 6);
|
|
53
|
+
var deposit = parseUnits(DEPOSIT_USDC, 6);
|
|
54
|
+
async function readBalances() {
|
|
55
|
+
const [gas, erc20, bal] = await Promise.all([
|
|
56
|
+
pub.getBalance({ address: account.address }),
|
|
57
|
+
pub.readContract({
|
|
58
|
+
address: USDC,
|
|
59
|
+
abi: erc20Abi,
|
|
60
|
+
functionName: "balanceOf",
|
|
61
|
+
args: [account.address]
|
|
62
|
+
}),
|
|
63
|
+
gateway.getBalances()
|
|
64
|
+
]);
|
|
65
|
+
return { gas, erc20, available: bal.gateway.available };
|
|
66
|
+
}
|
|
67
|
+
async function getStatus() {
|
|
68
|
+
const { gas, erc20, available } = await readBalances();
|
|
69
|
+
const ready = available >= fee;
|
|
70
|
+
let instructions;
|
|
71
|
+
if (ready) {
|
|
72
|
+
const calls = Math.floor(Number(formatUnits(available, 6)) / FEE_USDC);
|
|
73
|
+
instructions = `Ready \u2014 ${formatUnits(available, 6)} USDC in Gateway, ~${calls} Keryx calls.`;
|
|
74
|
+
} else if (erc20 >= deposit && gas > 0n) {
|
|
75
|
+
instructions = `You hold ${formatUnits(erc20, 6)} USDC but it isn't in the Gateway yet. ask_keryx will auto-deposit ${DEPOSIT_USDC} USDC on the next call.`;
|
|
76
|
+
} else if (erc20 >= deposit) {
|
|
77
|
+
instructions = `You hold ${formatUnits(erc20, 6)} USDC but no gas to deposit it. Fund ${account.address} with a little Arc-testnet gas at ${FAUCET} (Arc Testnet), then retry.`;
|
|
78
|
+
} else {
|
|
79
|
+
instructions = `Fund this address with Arc-testnet USDC, then call again:
|
|
80
|
+
1. Open ${FAUCET} \u2192 select Arc Testnet \u2192 paste ${account.address}
|
|
81
|
+
2. The faucet sends test USDC (also covers gas).
|
|
82
|
+
Each Keryx call costs ${FEE_USDC} USDC, paid from YOUR wallet \u2014 visible live on ${BASE_URL}/dashboard.`;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
address: account.address,
|
|
86
|
+
gasBalance: formatUnits(gas, 18),
|
|
87
|
+
usdcBalance: formatUnits(erc20, 6),
|
|
88
|
+
gatewayAvailable: formatUnits(available, 6),
|
|
89
|
+
ready,
|
|
90
|
+
instructions
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function ensureFunded() {
|
|
94
|
+
const first = await gateway.getBalances();
|
|
95
|
+
if (first.gateway.available >= fee) return;
|
|
96
|
+
const erc20 = await pub.readContract({
|
|
97
|
+
address: USDC,
|
|
98
|
+
abi: erc20Abi,
|
|
99
|
+
functionName: "balanceOf",
|
|
100
|
+
args: [account.address]
|
|
101
|
+
});
|
|
102
|
+
if (erc20 < deposit) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Insufficient testnet USDC. Fund ${account.address} at ${FAUCET} (Arc Testnet), then retry. Need \u2265 ${DEPOSIT_USDC} USDC (have ${formatUnits(erc20, 6)}).`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
await gateway.deposit(DEPOSIT_USDC);
|
|
108
|
+
for (let i = 0; i < 30; i++) {
|
|
109
|
+
const b = await gateway.getBalances();
|
|
110
|
+
if (b.gateway.available >= fee) return;
|
|
111
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
112
|
+
}
|
|
113
|
+
throw new Error("Gateway deposit didn't confirm in time \u2014 check balance and retry.");
|
|
114
|
+
}
|
|
115
|
+
async function askKeryx(question, budget) {
|
|
116
|
+
await ensureFunded();
|
|
117
|
+
const r = await gateway.pay(`${BASE_URL}/api/agent/ask`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: { question, ...budget ? { budget } : {} }
|
|
120
|
+
});
|
|
121
|
+
return { ...r.data, settlementId: String(r.transaction), amountPaid: r.formattedAmount };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// keryx-mcp-server.mts
|
|
125
|
+
var server = new McpServer({ name: "keryx", version: "0.1.0" });
|
|
126
|
+
server.registerTool(
|
|
127
|
+
"ask_keryx",
|
|
128
|
+
{
|
|
129
|
+
title: "Ask Keryx",
|
|
130
|
+
description: `Ask Keryx \u2014 an autonomous research agent that buys paid sources under a budget, answers with inline citations, and pays each cited creator in USDC on Arc. Costs ${meta.feeUsdc} USDC per call, paid from your own funded Arc-testnet wallet (run keryx_wallet_status first to fund it). Use when you want a grounded, source-cited answer AND the creators paid for their work.`,
|
|
131
|
+
inputSchema: {
|
|
132
|
+
question: z.string().min(3).describe("The research question to ask Keryx."),
|
|
133
|
+
budget: z.number().positive().optional().describe("Optional USDC budget Keryx may spend buying sources (default ~0.05).")
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async ({ question, budget }) => {
|
|
137
|
+
try {
|
|
138
|
+
const r = await askKeryx(question, budget);
|
|
139
|
+
const cites = r.citations?.length ? r.citations.map((c) => ` \u2022 ${c.source} \u2014 $${c.reward}`).join("\n") : " (none)";
|
|
140
|
+
const proof = r.settlementId ? ` (Circle Gateway settlement ${r.settlementId.slice(0, 12)}\u2026, batched on Arc)` : "";
|
|
141
|
+
const text = `${r.answer}
|
|
142
|
+
|
|
143
|
+
\u2014 Paid Keryx ${r.amountPaid ?? meta.feeUsdc} USDC${proof}
|
|
144
|
+
Keryx paid ${r.creatorsPaid} creator(s) $${r.totalToCreators} downstream:
|
|
145
|
+
${cites}
|
|
146
|
+
On-chain proof + live feed: ${meta.baseUrl}/dashboard`;
|
|
147
|
+
return { content: [{ type: "text", text }] };
|
|
148
|
+
} catch (e) {
|
|
149
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
150
|
+
return { isError: true, content: [{ type: "text", text: `Keryx call failed: ${msg}` }] };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
server.registerTool(
|
|
155
|
+
"keryx_wallet_status",
|
|
156
|
+
{
|
|
157
|
+
title: "Keryx wallet status",
|
|
158
|
+
description: "Show the Arc-testnet wallet the Keryx MCP server pays from: address, balances, whether it's ready, and exactly how to fund it via the Circle faucet. Run this before ask_keryx.",
|
|
159
|
+
inputSchema: {}
|
|
160
|
+
},
|
|
161
|
+
async () => {
|
|
162
|
+
try {
|
|
163
|
+
const s = await getStatus();
|
|
164
|
+
const text = `Keryx buyer wallet
|
|
165
|
+
address: ${s.address}
|
|
166
|
+
USDC: ${s.usdcBalance}
|
|
167
|
+
gas: ${s.gasBalance}
|
|
168
|
+
Gateway: ${s.gatewayAvailable} USDC available
|
|
169
|
+
ready: ${s.ready ? "yes" : "no"}
|
|
170
|
+
|
|
171
|
+
${s.instructions}`;
|
|
172
|
+
return { content: [{ type: "text", text }] };
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
175
|
+
return {
|
|
176
|
+
isError: true,
|
|
177
|
+
content: [{ type: "text", text: `Status check failed: ${msg}` }]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
var transport = new StdioServerTransport();
|
|
183
|
+
await server.connect(transport);
|
|
184
|
+
console.error(`Keryx MCP server ready \xB7 paying from ${meta.address} \u2192 ${meta.baseUrl}`);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "keryx-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add Keryx to any MCP agent — ask a research question, pay the x402 toll from your own Arc wallet, and Keryx answers with citations and pays every creator it cites in USDC on Arc.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"keryx-mcp": "dist/keryx-mcp.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "esbuild keryx-mcp-server.mts --bundle --platform=node --format=esm --target=node20 --packages=external --outfile=dist/keryx-mcp.mjs",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"x402",
|
|
24
|
+
"circle",
|
|
25
|
+
"arc",
|
|
26
|
+
"usdc",
|
|
27
|
+
"ai-agent",
|
|
28
|
+
"citations",
|
|
29
|
+
"micropayments",
|
|
30
|
+
"keryx"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://keryx.cc",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/tang-vu/keryx.git",
|
|
37
|
+
"directory": "mcp"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@circle-fin/x402-batching": "^2.1.0",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"viem": "^2.52.2",
|
|
43
|
+
"zod": "^3.25.76"
|
|
44
|
+
}
|
|
45
|
+
}
|