crossfin-mcp 1.8.3 → 1.8.5
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 +52 -96
- package/dist/index.js +490 -464
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,85 +1,14 @@
|
|
|
1
1
|
# CrossFin MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Give your AI agent access to Asian crypto markets.** 16 tools for real-time Korean exchange data, cross-exchange routing, and x402 paid API execution.
|
|
4
4
|
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
Use directly with `npx`:
|
|
5
|
+
## Install
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
npx -y crossfin-mcp
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm i -g crossfin-mcp
|
|
17
|
-
crossfin-mcp
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Tools
|
|
21
|
-
|
|
22
|
-
### Local ledger
|
|
23
|
-
|
|
24
|
-
- `create_wallet`
|
|
25
|
-
- `get_balance`
|
|
26
|
-
- `transfer`
|
|
27
|
-
- `list_transactions`
|
|
28
|
-
- `set_budget`
|
|
29
|
-
|
|
30
|
-
### CrossFin API (live)
|
|
31
|
-
|
|
32
|
-
- `search_services`
|
|
33
|
-
- `list_services`
|
|
34
|
-
- `get_service`
|
|
35
|
-
- `list_categories`
|
|
36
|
-
- `get_kimchi_premium`
|
|
37
|
-
- `get_analytics`
|
|
38
|
-
- `get_guide`
|
|
39
|
-
|
|
40
|
-
### Routing engine
|
|
41
|
-
|
|
42
|
-
- `find_optimal_route` — find optimal crypto transfer route across 5 exchanges (paid via x402, requires `EVM_PRIVATE_KEY`)
|
|
43
|
-
- `list_exchange_fees` — compare trading and withdrawal fees across exchanges
|
|
44
|
-
- `compare_exchange_prices` — compare live prices for a coin across Korean exchanges
|
|
45
|
-
|
|
46
|
-
### Paid execution
|
|
47
|
-
|
|
48
|
-
- `call_paid_service` — call any CrossFin paid endpoint with automatic x402 USDC payment on Base (requires `EVM_PRIVATE_KEY`)
|
|
49
|
-
|
|
50
|
-
## Run (dev)
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
cd apps/mcp-server
|
|
54
|
-
npm install
|
|
55
|
-
npm run dev
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Ledger storage
|
|
59
|
-
|
|
60
|
-
By default the server stores data at:
|
|
61
|
-
|
|
62
|
-
- `~/.crossfin/ledger.json`
|
|
63
|
-
|
|
64
|
-
Override with:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
export CROSSFIN_LEDGER_PATH="/path/to/ledger.json"
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## API base URL
|
|
71
|
-
|
|
72
|
-
By default the server calls the live CrossFin API at `https://crossfin.dev`.
|
|
73
|
-
|
|
74
|
-
Override with:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
export CROSSFIN_API_URL="https://crossfin.dev"
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Claude Desktop config (example)
|
|
81
|
-
|
|
82
|
-
Use the published npm package:
|
|
11
|
+
### Claude Desktop config
|
|
83
12
|
|
|
84
13
|
```json
|
|
85
14
|
{
|
|
@@ -88,7 +17,6 @@ Use the published npm package:
|
|
|
88
17
|
"command": "npx",
|
|
89
18
|
"args": ["-y", "crossfin-mcp"],
|
|
90
19
|
"env": {
|
|
91
|
-
"CROSSFIN_API_URL": "https://crossfin.dev",
|
|
92
20
|
"EVM_PRIVATE_KEY": "0x..."
|
|
93
21
|
}
|
|
94
22
|
}
|
|
@@ -96,27 +24,55 @@ Use the published npm package:
|
|
|
96
24
|
}
|
|
97
25
|
```
|
|
98
26
|
|
|
99
|
-
|
|
27
|
+
> **No EVM key?** Free tools work without one. Paid tools ($0.01–$0.10 per call) require a Base wallet with USDC.
|
|
100
28
|
|
|
101
|
-
|
|
102
|
-
{
|
|
103
|
-
"mcpServers": {
|
|
104
|
-
"crossfin": {
|
|
105
|
-
"command": "node",
|
|
106
|
-
"args": ["/ABS/PATH/TO/crossfin/apps/mcp-server/dist/index.js"],
|
|
107
|
-
"env": {
|
|
108
|
-
"CROSSFIN_LEDGER_PATH": "/ABS/PATH/TO/ledger.json",
|
|
109
|
-
"CROSSFIN_API_URL": "https://crossfin.dev",
|
|
110
|
-
"EVM_PRIVATE_KEY": "0x..."
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
```
|
|
29
|
+
## What your agent can do
|
|
116
30
|
|
|
117
|
-
|
|
31
|
+
- **"빗썸에서 바이낸스로 500만원 USDC 만들려면?"** → `find_optimal_route` evaluates 11 bridge coins, returns cheapest path
|
|
32
|
+
- **"김치 프리미엄 얼마야?"** → `get_kimchi_premium` returns real-time spread across 11 pairs
|
|
33
|
+
- **"거래소별 XRP 가격 비교해줘"** → `compare_exchange_prices` checks 4 Korean exchanges
|
|
34
|
+
- **"한국 시장 브리핑해줘"** → `call_paid_service` calls Morning Brief bundle
|
|
118
35
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
| Tool | Free/Paid | Description |
|
|
39
|
+
|------|-----------|-------------|
|
|
40
|
+
| `find_optimal_route` | $0.10 | Optimal crypto transfer path across 5 exchanges (11 bridge coins) |
|
|
41
|
+
| `list_exchange_fees` | Free | Trading + withdrawal fee comparison |
|
|
42
|
+
| `compare_exchange_prices` | Free | Live price comparison across Korean exchanges |
|
|
43
|
+
| `get_kimchi_premium` | Free | Korean vs. global price spread preview |
|
|
44
|
+
| `call_paid_service` | Varies | Call any of 35 paid APIs with automatic x402 payment |
|
|
45
|
+
| `search_services` | Free | Search 184 registered services |
|
|
46
|
+
| `list_services` | Free | Browse service catalog |
|
|
47
|
+
| `get_service` | Free | Service details |
|
|
48
|
+
| `list_categories` | Free | Service categories |
|
|
49
|
+
| `get_guide` | Free | Full agent guide |
|
|
50
|
+
| `get_analytics` | Free | Gateway usage stats |
|
|
51
|
+
| `create_wallet` | Free | Local ledger wallet |
|
|
52
|
+
| `get_balance` | Free | Check wallet balance |
|
|
53
|
+
| `transfer` | Free | Transfer between wallets |
|
|
54
|
+
| `list_transactions` | Free | Transaction history |
|
|
55
|
+
| `set_budget` | Free | Daily spend limit |
|
|
56
|
+
|
|
57
|
+
## Supported exchanges
|
|
58
|
+
|
|
59
|
+
Bithumb, Upbit, Coinone, GoPax (Korea) + Binance (Global)
|
|
60
|
+
|
|
61
|
+
## Bridge coins
|
|
62
|
+
|
|
63
|
+
BTC, ETH, XRP, SOL, DOGE, ADA, DOT, LINK, AVAX, TRX, KAIA
|
|
64
|
+
|
|
65
|
+
## Environment variables
|
|
66
|
+
|
|
67
|
+
| Variable | Required | Description |
|
|
68
|
+
|----------|----------|-------------|
|
|
69
|
+
| `EVM_PRIVATE_KEY` | For paid tools | Base wallet private key for x402 USDC payments |
|
|
70
|
+
| `CROSSFIN_API_URL` | No | API base URL (default: `https://crossfin.dev`) |
|
|
71
|
+
| `CROSSFIN_LEDGER_PATH` | No | Local ledger path (default: `~/.crossfin/ledger.json`) |
|
|
72
|
+
|
|
73
|
+
## Links
|
|
74
|
+
|
|
75
|
+
- [crossfin.dev](https://crossfin.dev) — Dashboard
|
|
76
|
+
- [live.crossfin.dev](https://live.crossfin.dev) — Live routing demo
|
|
77
|
+
- [GitHub](https://github.com/bubilife1202/crossfin) — Source code
|
|
78
|
+
- [npm](https://www.npmjs.com/package/crossfin-mcp) — Package
|
package/dist/index.js
CHANGED
|
@@ -7,505 +7,531 @@ import { registerExactEvmScheme } from '@x402/evm/exact/client';
|
|
|
7
7
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { createWallet, defaultLedgerPath, getBalance, listTransactions, setBudget, transfer, } from './ledgerStore.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function ensureCrossfinPaidUrl(raw) {
|
|
18
|
-
let url;
|
|
19
|
-
try {
|
|
20
|
-
url = new URL(raw.trim());
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
throw new Error('Invalid URL');
|
|
24
|
-
}
|
|
25
|
-
if (url.protocol !== 'https:') {
|
|
26
|
-
throw new Error('Only https:// URLs are allowed');
|
|
27
|
-
}
|
|
28
|
-
if (url.username || url.password) {
|
|
29
|
-
throw new Error('Credentials in URL are not allowed');
|
|
30
|
-
}
|
|
31
|
-
if (url.origin !== API_ORIGIN) {
|
|
32
|
-
throw new Error(`Only ${API_ORIGIN} URLs are allowed`);
|
|
33
|
-
}
|
|
34
|
-
if (!url.pathname.startsWith('/api/premium/')) {
|
|
35
|
-
throw new Error('Only /api/premium/* endpoints are allowed');
|
|
36
|
-
}
|
|
37
|
-
return url.toString();
|
|
38
|
-
}
|
|
39
|
-
/* ── x402 paid fetch setup ── */
|
|
40
|
-
let paidFetch = null;
|
|
41
|
-
let httpClient = null;
|
|
42
|
-
let payerAddress = '';
|
|
43
|
-
if (EVM_PRIVATE_KEY) {
|
|
44
|
-
const signer = privateKeyToAccount(EVM_PRIVATE_KEY);
|
|
45
|
-
payerAddress = signer.address;
|
|
46
|
-
const client = new x402Client();
|
|
47
|
-
registerExactEvmScheme(client, { signer });
|
|
48
|
-
paidFetch = wrapFetchWithPayment(fetch, client);
|
|
49
|
-
httpClient = new x402HTTPClient(client);
|
|
10
|
+
/* ── Version ── */
|
|
11
|
+
let MCP_VERSION = '0.0.0';
|
|
12
|
+
try {
|
|
13
|
+
const _require = createRequire(import.meta.url || 'file:///fallback');
|
|
14
|
+
const pkg = _require('../package.json');
|
|
15
|
+
if (typeof pkg.version === 'string' && pkg.version.trim())
|
|
16
|
+
MCP_VERSION = pkg.version.trim();
|
|
50
17
|
}
|
|
51
|
-
|
|
52
|
-
|
|
18
|
+
catch { /* CJS / bundler fallback */ }
|
|
19
|
+
/* ── Smithery session config ── */
|
|
20
|
+
export const configSchema = z.object({
|
|
21
|
+
evmPrivateKey: z.string().optional().describe('Base wallet private key for x402 USDC payments (optional — free tools work without it)'),
|
|
22
|
+
apiUrl: z.string().optional().describe('CrossFin API base URL (default: https://crossfin.dev)'),
|
|
23
|
+
ledgerPath: z.string().optional().describe('Local ledger file path'),
|
|
24
|
+
});
|
|
25
|
+
/* ── Server factory (Smithery SDK compatible) ── */
|
|
26
|
+
export default function createServer({ config }) {
|
|
27
|
+
const LEDGER_PATH = config.ledgerPath?.trim() || defaultLedgerPath();
|
|
28
|
+
const API_BASE = (config.apiUrl?.trim() || 'https://crossfin.dev').replace(/\/$/, '');
|
|
29
|
+
const EVM_PRIVATE_KEY = config.evmPrivateKey?.trim() ?? '';
|
|
30
|
+
const API_ORIGIN = new URL(API_BASE).origin;
|
|
31
|
+
function ensureCrossfinPaidUrl(raw) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(raw.trim());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error('Invalid URL');
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol !== 'https:') {
|
|
40
|
+
throw new Error('Only https:// URLs are allowed');
|
|
41
|
+
}
|
|
42
|
+
if (url.username || url.password) {
|
|
43
|
+
throw new Error('Credentials in URL are not allowed');
|
|
44
|
+
}
|
|
45
|
+
if (url.origin !== API_ORIGIN) {
|
|
46
|
+
throw new Error(`Only ${API_ORIGIN} URLs are allowed`);
|
|
47
|
+
}
|
|
48
|
+
if (!url.pathname.startsWith('/api/premium/')) {
|
|
49
|
+
throw new Error('Only /api/premium/* endpoints are allowed');
|
|
50
|
+
}
|
|
51
|
+
return url.toString();
|
|
52
|
+
}
|
|
53
|
+
/* ── x402 paid fetch setup ── */
|
|
54
|
+
let paidFetch = null;
|
|
55
|
+
let httpClient = null;
|
|
56
|
+
let payerAddress = '';
|
|
57
|
+
if (EVM_PRIVATE_KEY) {
|
|
58
|
+
const signer = privateKeyToAccount(EVM_PRIVATE_KEY);
|
|
59
|
+
payerAddress = signer.address;
|
|
60
|
+
const client = new x402Client();
|
|
61
|
+
registerExactEvmScheme(client, { signer });
|
|
62
|
+
paidFetch = wrapFetchWithPayment(fetch, client);
|
|
63
|
+
httpClient = new x402HTTPClient(client);
|
|
64
|
+
}
|
|
65
|
+
function basescanLink(networkId, txHash) {
|
|
66
|
+
if (!txHash)
|
|
67
|
+
return null;
|
|
68
|
+
if (networkId === 'eip155:84532')
|
|
69
|
+
return `https://sepolia.basescan.org/tx/${txHash}`;
|
|
70
|
+
if (networkId === 'eip155:8453')
|
|
71
|
+
return `https://basescan.org/tx/${txHash}`;
|
|
53
72
|
return null;
|
|
54
|
-
if (networkId === 'eip155:84532')
|
|
55
|
-
return `https://sepolia.basescan.org/tx/${txHash}`;
|
|
56
|
-
if (networkId === 'eip155:8453')
|
|
57
|
-
return `https://basescan.org/tx/${txHash}`;
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
const server = new McpServer({ name: 'crossfin', version: MCP_VERSION });
|
|
61
|
-
const railSchema = z.enum(['manual', 'kakaopay', 'toss', 'stripe', 'x402']);
|
|
62
|
-
async function apiFetch(path) {
|
|
63
|
-
const res = await fetch(`${API_BASE}${path}`);
|
|
64
|
-
if (!res.ok) {
|
|
65
|
-
throw new Error(`API ${res.status}: ${await res.text().catch(() => res.statusText)}`);
|
|
66
73
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
description: 'Get the balance (KRW) of a wallet',
|
|
92
|
-
inputSchema: z.object({ walletId: z.string().min(1) }),
|
|
93
|
-
outputSchema: z.object({ walletId: z.string(), balanceKrw: z.number() }),
|
|
94
|
-
}, async ({ walletId }) => {
|
|
95
|
-
const balance = await getBalance(LEDGER_PATH, walletId);
|
|
96
|
-
if (balance === null) {
|
|
74
|
+
const server = new McpServer({ name: 'crossfin', version: MCP_VERSION });
|
|
75
|
+
const railSchema = z.enum(['manual', 'kakaopay', 'toss', 'stripe', 'x402']);
|
|
76
|
+
async function apiFetch(path) {
|
|
77
|
+
const res = await fetch(`${API_BASE}${path}`);
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(`API ${res.status}: ${await res.text().catch(() => res.statusText)}`);
|
|
80
|
+
}
|
|
81
|
+
return res.json();
|
|
82
|
+
}
|
|
83
|
+
server.registerTool('create_wallet', {
|
|
84
|
+
title: 'Create wallet',
|
|
85
|
+
description: 'Create a wallet in the local CrossFin ledger',
|
|
86
|
+
inputSchema: z.object({
|
|
87
|
+
label: z.string().min(1).describe('Wallet label'),
|
|
88
|
+
initialDepositKrw: z.number().optional().describe('Optional initial deposit (KRW)'),
|
|
89
|
+
}),
|
|
90
|
+
outputSchema: z.object({
|
|
91
|
+
walletId: z.string(),
|
|
92
|
+
label: z.string(),
|
|
93
|
+
balanceKrw: z.number(),
|
|
94
|
+
}),
|
|
95
|
+
}, async ({ label, initialDepositKrw }) => {
|
|
96
|
+
const wallet = await createWallet(LEDGER_PATH, label, initialDepositKrw ?? 0);
|
|
97
|
+
const out = { walletId: wallet.id, label: wallet.label, balanceKrw: wallet.balanceKrw };
|
|
97
98
|
return {
|
|
98
|
-
content: [{ type: 'text', text:
|
|
99
|
-
structuredContent:
|
|
99
|
+
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
100
|
+
structuredContent: out,
|
|
100
101
|
};
|
|
101
|
-
}
|
|
102
|
-
const out = { walletId, balanceKrw: balance };
|
|
103
|
-
return {
|
|
104
|
-
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
105
|
-
structuredContent: out,
|
|
106
|
-
};
|
|
107
|
-
});
|
|
108
|
-
server.registerTool('transfer', {
|
|
109
|
-
title: 'Transfer',
|
|
110
|
-
description: 'Transfer funds between wallets (KRW)',
|
|
111
|
-
inputSchema: z.object({
|
|
112
|
-
fromWalletId: z.string().min(1),
|
|
113
|
-
toWalletId: z.string().min(1),
|
|
114
|
-
amountKrw: z.number().describe('Transfer amount in KRW'),
|
|
115
|
-
rail: railSchema.optional().describe('Payment rail'),
|
|
116
|
-
memo: z.string().optional().describe('Memo'),
|
|
117
|
-
}),
|
|
118
|
-
outputSchema: z.object({
|
|
119
|
-
transactionId: z.string(),
|
|
120
|
-
fromBalanceKrw: z.number(),
|
|
121
|
-
toBalanceKrw: z.number(),
|
|
122
|
-
}),
|
|
123
|
-
}, async ({ fromWalletId, toWalletId, amountKrw, rail, memo }) => {
|
|
124
|
-
const result = await transfer(LEDGER_PATH, {
|
|
125
|
-
fromWalletId,
|
|
126
|
-
toWalletId,
|
|
127
|
-
amountKrw,
|
|
128
|
-
rail: (rail ?? 'manual'),
|
|
129
|
-
memo: memo ?? '',
|
|
130
102
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
return {
|
|
147
|
-
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
148
|
-
structuredContent: out,
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
server.registerTool('list_transactions', {
|
|
152
|
-
title: 'List transactions',
|
|
153
|
-
description: 'List transactions (optionally filtered by wallet)',
|
|
154
|
-
inputSchema: z.object({
|
|
155
|
-
walletId: z.string().optional(),
|
|
156
|
-
limit: z.number().optional().describe('Max items (1..200). Default 50'),
|
|
157
|
-
}),
|
|
158
|
-
}, async ({ walletId, limit }) => {
|
|
159
|
-
const trimmedWalletId = walletId?.trim();
|
|
160
|
-
const txs = await listTransactions(LEDGER_PATH, trimmedWalletId ? { walletId: trimmedWalletId, limit: limit ?? 50 } : { limit: limit ?? 50 });
|
|
161
|
-
return {
|
|
162
|
-
content: [{ type: 'text', text: JSON.stringify({ transactions: txs }) }],
|
|
163
|
-
structuredContent: { transactions: txs },
|
|
164
|
-
};
|
|
165
|
-
});
|
|
166
|
-
server.registerTool('set_budget', {
|
|
167
|
-
title: 'Set budget',
|
|
168
|
-
description: 'Set a daily spend limit for the local ledger (KRW)',
|
|
169
|
-
inputSchema: z.object({
|
|
170
|
-
dailyLimitKrw: z.number().nullable().describe('Daily budget limit (KRW). Use null to clear.'),
|
|
171
|
-
}),
|
|
172
|
-
outputSchema: z.object({ dailyLimitKrw: z.number().nullable() }),
|
|
173
|
-
}, async ({ dailyLimitKrw }) => {
|
|
174
|
-
const out = await setBudget(LEDGER_PATH, dailyLimitKrw);
|
|
175
|
-
return {
|
|
176
|
-
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
177
|
-
structuredContent: out,
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
server.registerTool('search_services', {
|
|
181
|
-
title: 'Search services',
|
|
182
|
-
description: 'Search the CrossFin service registry for x402 services by keyword',
|
|
183
|
-
inputSchema: z.object({
|
|
184
|
-
query: z.string().describe('Search keyword (e.g. "crypto", "translate", "korea")'),
|
|
185
|
-
}),
|
|
186
|
-
}, async ({ query }) => {
|
|
187
|
-
try {
|
|
188
|
-
const qs = new URLSearchParams({ q: query });
|
|
189
|
-
const data = await apiFetch(`/api/registry/search?${qs.toString()}`);
|
|
190
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
191
|
-
}
|
|
192
|
-
catch (e) {
|
|
193
|
-
return {
|
|
194
|
-
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
195
|
-
isError: true,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
server.registerTool('list_services', {
|
|
200
|
-
title: 'List services',
|
|
201
|
-
description: 'List services from the CrossFin registry with optional category filter',
|
|
202
|
-
inputSchema: z.object({
|
|
203
|
-
category: z.string().optional().describe('Category filter (e.g. "crypto-data", "ai", "tools")'),
|
|
204
|
-
limit: z.number().int().min(1).max(100).optional().describe('Max results (default 20, max 100)'),
|
|
205
|
-
}),
|
|
206
|
-
}, async ({ category, limit }) => {
|
|
207
|
-
try {
|
|
208
|
-
const qs = new URLSearchParams();
|
|
209
|
-
const trimmedCategory = category?.trim();
|
|
210
|
-
if (trimmedCategory)
|
|
211
|
-
qs.set('category', trimmedCategory);
|
|
212
|
-
if (typeof limit === 'number')
|
|
213
|
-
qs.set('limit', String(limit));
|
|
214
|
-
const path = qs.size ? `/api/registry?${qs.toString()}` : '/api/registry';
|
|
215
|
-
const data = await apiFetch(path);
|
|
216
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
217
|
-
}
|
|
218
|
-
catch (e) {
|
|
219
|
-
return {
|
|
220
|
-
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
221
|
-
isError: true,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
server.registerTool('get_service', {
|
|
226
|
-
title: 'Get service',
|
|
227
|
-
description: 'Get detailed information about a specific service by ID',
|
|
228
|
-
inputSchema: z.object({
|
|
229
|
-
serviceId: z.string().describe('Service ID (e.g. "svc_kimchi_premium")'),
|
|
230
|
-
}),
|
|
231
|
-
}, async ({ serviceId }) => {
|
|
232
|
-
try {
|
|
233
|
-
const encodedId = encodeURIComponent(serviceId);
|
|
234
|
-
const data = await apiFetch(`/api/registry/${encodedId}`);
|
|
235
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
return {
|
|
239
|
-
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
240
|
-
isError: true,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
server.registerTool('list_categories', {
|
|
245
|
-
title: 'List categories',
|
|
246
|
-
description: 'List all service categories with counts',
|
|
247
|
-
inputSchema: z.object({}),
|
|
248
|
-
}, async (_params) => {
|
|
249
|
-
try {
|
|
250
|
-
const data = await apiFetch('/api/registry/categories');
|
|
251
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
252
|
-
}
|
|
253
|
-
catch (e) {
|
|
103
|
+
server.registerTool('get_balance', {
|
|
104
|
+
title: 'Get balance',
|
|
105
|
+
description: 'Get the balance (KRW) of a wallet',
|
|
106
|
+
inputSchema: z.object({ walletId: z.string().min(1) }),
|
|
107
|
+
outputSchema: z.object({ walletId: z.string(), balanceKrw: z.number() }),
|
|
108
|
+
}, async ({ walletId }) => {
|
|
109
|
+
const balance = await getBalance(LEDGER_PATH, walletId);
|
|
110
|
+
if (balance === null) {
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: 'text', text: `Wallet not found: ${walletId}` }],
|
|
113
|
+
structuredContent: { walletId, balanceKrw: -1 },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const out = { walletId, balanceKrw: balance };
|
|
254
117
|
return {
|
|
255
|
-
content: [{ type: 'text', text:
|
|
256
|
-
|
|
118
|
+
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
119
|
+
structuredContent: out,
|
|
257
120
|
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
121
|
+
});
|
|
122
|
+
server.registerTool('transfer', {
|
|
123
|
+
title: 'Transfer',
|
|
124
|
+
description: 'Transfer funds between wallets (KRW)',
|
|
125
|
+
inputSchema: z.object({
|
|
126
|
+
fromWalletId: z.string().min(1),
|
|
127
|
+
toWalletId: z.string().min(1),
|
|
128
|
+
amountKrw: z.number().describe('Transfer amount in KRW'),
|
|
129
|
+
rail: railSchema.optional().describe('Payment rail'),
|
|
130
|
+
memo: z.string().optional().describe('Memo'),
|
|
131
|
+
}),
|
|
132
|
+
outputSchema: z.object({
|
|
133
|
+
transactionId: z.string(),
|
|
134
|
+
fromBalanceKrw: z.number(),
|
|
135
|
+
toBalanceKrw: z.number(),
|
|
136
|
+
}),
|
|
137
|
+
}, async ({ fromWalletId, toWalletId, amountKrw, rail, memo }) => {
|
|
138
|
+
const result = await transfer(LEDGER_PATH, {
|
|
139
|
+
fromWalletId,
|
|
140
|
+
toWalletId,
|
|
141
|
+
amountKrw,
|
|
142
|
+
rail: (rail ?? 'manual'),
|
|
143
|
+
memo: memo ?? '',
|
|
144
|
+
});
|
|
145
|
+
if (!result) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text', text: 'Transfer failed (check wallet ids / balance / amount)' }],
|
|
148
|
+
structuredContent: {
|
|
149
|
+
transactionId: '',
|
|
150
|
+
fromBalanceKrw: -1,
|
|
151
|
+
toBalanceKrw: -1,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const out = {
|
|
156
|
+
transactionId: result.tx.id,
|
|
157
|
+
fromBalanceKrw: result.fromBalanceKrw,
|
|
158
|
+
toBalanceKrw: result.toBalanceKrw,
|
|
273
159
|
};
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
server.registerTool('get_analytics', {
|
|
277
|
-
title: 'Get analytics',
|
|
278
|
-
description: 'Get CrossFin gateway usage analytics — total calls, top services, recent activity',
|
|
279
|
-
inputSchema: z.object({}),
|
|
280
|
-
}, async (_params) => {
|
|
281
|
-
try {
|
|
282
|
-
const data = await apiFetch('/api/analytics/overview');
|
|
283
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
160
|
return {
|
|
287
|
-
content: [{ type: 'text', text:
|
|
288
|
-
|
|
161
|
+
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
162
|
+
structuredContent: out,
|
|
289
163
|
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
catch (e) {
|
|
164
|
+
});
|
|
165
|
+
server.registerTool('list_transactions', {
|
|
166
|
+
title: 'List transactions',
|
|
167
|
+
description: 'List transactions (optionally filtered by wallet)',
|
|
168
|
+
inputSchema: z.object({
|
|
169
|
+
walletId: z.string().optional(),
|
|
170
|
+
limit: z.number().optional().describe('Max items (1..200). Default 50'),
|
|
171
|
+
}),
|
|
172
|
+
}, async ({ walletId, limit }) => {
|
|
173
|
+
const trimmedWalletId = walletId?.trim();
|
|
174
|
+
const txs = await listTransactions(LEDGER_PATH, trimmedWalletId ? { walletId: trimmedWalletId, limit: limit ?? 50 } : { limit: limit ?? 50 });
|
|
302
175
|
return {
|
|
303
|
-
content: [{ type: 'text', text:
|
|
304
|
-
|
|
176
|
+
content: [{ type: 'text', text: JSON.stringify({ transactions: txs }) }],
|
|
177
|
+
structuredContent: { transactions: txs },
|
|
305
178
|
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
params: z.record(z.string(), z.string()).optional().describe('Query parameters'),
|
|
317
|
-
}),
|
|
318
|
-
}, async ({ serviceId, url, params }) => {
|
|
319
|
-
if (!paidFetch || !httpClient) {
|
|
179
|
+
});
|
|
180
|
+
server.registerTool('set_budget', {
|
|
181
|
+
title: 'Set budget',
|
|
182
|
+
description: 'Set a daily spend limit for the local ledger (KRW)',
|
|
183
|
+
inputSchema: z.object({
|
|
184
|
+
dailyLimitKrw: z.number().nullable().describe('Daily budget limit (KRW). Use null to clear.'),
|
|
185
|
+
}),
|
|
186
|
+
outputSchema: z.object({ dailyLimitKrw: z.number().nullable() }),
|
|
187
|
+
}, async ({ dailyLimitKrw }) => {
|
|
188
|
+
const out = await setBudget(LEDGER_PATH, dailyLimitKrw);
|
|
320
189
|
return {
|
|
321
|
-
content: [{ type: 'text', text:
|
|
322
|
-
|
|
190
|
+
content: [{ type: 'text', text: JSON.stringify(out) }],
|
|
191
|
+
structuredContent: out,
|
|
323
192
|
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
193
|
+
});
|
|
194
|
+
server.registerTool('search_services', {
|
|
195
|
+
title: 'Search services',
|
|
196
|
+
description: 'Search the CrossFin service registry for x402 services by keyword',
|
|
197
|
+
inputSchema: z.object({
|
|
198
|
+
query: z.string().describe('Search keyword (e.g. "crypto", "translate", "korea")'),
|
|
199
|
+
}),
|
|
200
|
+
}, async ({ query }) => {
|
|
330
201
|
try {
|
|
331
|
-
|
|
202
|
+
const qs = new URLSearchParams({ q: query });
|
|
203
|
+
const data = await apiFetch(`/api/registry/search?${qs.toString()}`);
|
|
204
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
332
205
|
}
|
|
333
206
|
catch (e) {
|
|
334
207
|
return {
|
|
335
|
-
content: [{ type: 'text', text: `
|
|
208
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
336
209
|
isError: true,
|
|
337
210
|
};
|
|
338
211
|
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
212
|
+
});
|
|
213
|
+
server.registerTool('list_services', {
|
|
214
|
+
title: 'List services',
|
|
215
|
+
description: 'List services from the CrossFin registry with optional category filter',
|
|
216
|
+
inputSchema: z.object({
|
|
217
|
+
category: z.string().optional().describe('Category filter (e.g. "crypto-data", "ai", "tools")'),
|
|
218
|
+
limit: z.number().int().min(1).max(100).optional().describe('Max results (default 20, max 100)'),
|
|
219
|
+
}),
|
|
220
|
+
}, async ({ category, limit }) => {
|
|
221
|
+
try {
|
|
222
|
+
const qs = new URLSearchParams();
|
|
223
|
+
const trimmedCategory = category?.trim();
|
|
224
|
+
if (trimmedCategory)
|
|
225
|
+
qs.set('category', trimmedCategory);
|
|
226
|
+
if (typeof limit === 'number')
|
|
227
|
+
qs.set('limit', String(limit));
|
|
228
|
+
const path = qs.size ? `/api/registry?${qs.toString()}` : '/api/registry';
|
|
229
|
+
const data = await apiFetch(path);
|
|
230
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
235
|
+
isError: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
server.registerTool('get_service', {
|
|
240
|
+
title: 'Get service',
|
|
241
|
+
description: 'Get detailed information about a specific service by ID',
|
|
242
|
+
inputSchema: z.object({
|
|
243
|
+
serviceId: z.string().describe('Service ID (e.g. "svc_kimchi_premium")'),
|
|
244
|
+
}),
|
|
245
|
+
}, async ({ serviceId }) => {
|
|
246
|
+
try {
|
|
247
|
+
const encodedId = encodeURIComponent(serviceId);
|
|
248
|
+
const data = await apiFetch(`/api/registry/${encodedId}`);
|
|
249
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
254
|
+
isError: true,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
server.registerTool('list_categories', {
|
|
259
|
+
title: 'List categories',
|
|
260
|
+
description: 'List all service categories with counts',
|
|
261
|
+
inputSchema: z.object({}),
|
|
262
|
+
}, async (_params) => {
|
|
263
|
+
try {
|
|
264
|
+
const data = await apiFetch('/api/registry/categories');
|
|
265
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
342
268
|
return {
|
|
343
|
-
content: [{ type: 'text', text:
|
|
269
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
344
270
|
isError: true,
|
|
345
271
|
};
|
|
346
272
|
}
|
|
273
|
+
});
|
|
274
|
+
server.registerTool('get_kimchi_premium', {
|
|
275
|
+
title: 'Get kimchi premium',
|
|
276
|
+
description: 'Get free preview of the Kimchi Premium index — real-time price spread between Korean and global crypto exchanges (top 3 pairs)',
|
|
277
|
+
inputSchema: z.object({}),
|
|
278
|
+
}, async (_params) => {
|
|
347
279
|
try {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
280
|
+
const data = await apiFetch('/api/arbitrage/demo');
|
|
281
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
return {
|
|
285
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
server.registerTool('get_analytics', {
|
|
291
|
+
title: 'Get analytics',
|
|
292
|
+
description: 'Get CrossFin gateway usage analytics — total calls, top services, recent activity',
|
|
293
|
+
inputSchema: z.object({}),
|
|
294
|
+
}, async (_params) => {
|
|
295
|
+
try {
|
|
296
|
+
const data = await apiFetch('/api/analytics/overview');
|
|
297
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
server.registerTool('get_guide', {
|
|
307
|
+
title: 'Get CrossFin guide',
|
|
308
|
+
description: 'Get the complete CrossFin API guide — what services are available, how to search, pricing, x402 payment flow, and code examples',
|
|
309
|
+
inputSchema: z.object({}),
|
|
310
|
+
}, async (_params) => {
|
|
311
|
+
try {
|
|
312
|
+
const data = await apiFetch('/api/docs/guide');
|
|
313
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
318
|
+
isError: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
server.registerTool('call_paid_service', {
|
|
323
|
+
title: 'Call paid service',
|
|
324
|
+
description: 'Call a CrossFin paid API endpoint with automatic x402 USDC payment on Base. ' +
|
|
325
|
+
'Requires EVM_PRIVATE_KEY env var with funded wallet. ' +
|
|
326
|
+
'Returns the API response data plus payment proof (txHash, basescan link).',
|
|
327
|
+
inputSchema: z.object({
|
|
328
|
+
serviceId: z.string().optional().describe('Service ID from registry — looks up endpoint/price automatically'),
|
|
329
|
+
url: z.string().optional().describe('Direct URL to call (use this OR serviceId, not both)'),
|
|
330
|
+
params: z.record(z.string(), z.string()).optional().describe('Query parameters'),
|
|
331
|
+
}),
|
|
332
|
+
}, async ({ serviceId, url, params }) => {
|
|
333
|
+
if (!paidFetch || !httpClient) {
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: 'text', text: 'EVM_PRIVATE_KEY not configured — cannot make paid calls' }],
|
|
336
|
+
isError: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (serviceId && url) {
|
|
340
|
+
return { content: [{ type: 'text', text: 'Provide serviceId or url, not both' }], isError: true };
|
|
341
|
+
}
|
|
342
|
+
let targetUrl;
|
|
343
|
+
if (url) {
|
|
344
|
+
try {
|
|
345
|
+
targetUrl = ensureCrossfinPaidUrl(url);
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
return {
|
|
349
|
+
content: [{ type: 'text', text: `Invalid url: ${e instanceof Error ? e.message : String(e)}` }],
|
|
350
|
+
isError: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else if (serviceId) {
|
|
355
|
+
if (!serviceId.startsWith('crossfin_')) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: 'text', text: 'Only crossfin_* serviceId values are allowed for paid calls' }],
|
|
358
|
+
isError: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const svc = await apiFetch(`/api/registry/${encodeURIComponent(serviceId)}`);
|
|
363
|
+
const endpoint = typeof svc.data?.endpoint === 'string' ? svc.data.endpoint : '';
|
|
364
|
+
if (!endpoint) {
|
|
365
|
+
return { content: [{ type: 'text', text: `Service ${serviceId} has no endpoint` }], isError: true };
|
|
366
|
+
}
|
|
367
|
+
targetUrl = ensureCrossfinPaidUrl(endpoint);
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: 'text', text: `Registry lookup failed: ${e instanceof Error ? e.message : String(e)}` }],
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
return { content: [{ type: 'text', text: 'Provide serviceId or url' }], isError: true };
|
|
378
|
+
}
|
|
379
|
+
if (params && Object.keys(params).length > 0) {
|
|
380
|
+
const qs = new URLSearchParams(params);
|
|
381
|
+
const sep = targetUrl.includes('?') ? '&' : '?';
|
|
382
|
+
targetUrl = `${targetUrl}${sep}${qs.toString()}`;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const res = await paidFetch(targetUrl, { method: 'GET' });
|
|
386
|
+
const body = await res.text();
|
|
387
|
+
let data;
|
|
388
|
+
try {
|
|
389
|
+
data = JSON.parse(body);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
data = body;
|
|
352
393
|
}
|
|
353
|
-
|
|
394
|
+
const settle = httpClient.getPaymentSettleResponse((name) => res.headers.get(name));
|
|
395
|
+
const txHash = settle?.transaction;
|
|
396
|
+
const networkId = settle?.network;
|
|
397
|
+
const scanLink = basescanLink(networkId, txHash);
|
|
398
|
+
const result = {
|
|
399
|
+
status: res.status,
|
|
400
|
+
payer: payerAddress,
|
|
401
|
+
paid: !!settle,
|
|
402
|
+
txHash: txHash ?? null,
|
|
403
|
+
basescan: scanLink,
|
|
404
|
+
data,
|
|
405
|
+
};
|
|
406
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
354
407
|
}
|
|
355
408
|
catch (e) {
|
|
356
409
|
return {
|
|
357
|
-
content: [{ type: 'text', text: `
|
|
410
|
+
content: [{ type: 'text', text: `Payment call failed: ${e instanceof Error ? e.message : String(e)}` }],
|
|
411
|
+
isError: true,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
/* ── Routing Engine Tools ── */
|
|
416
|
+
server.registerTool('find_optimal_route', {
|
|
417
|
+
title: 'Find optimal route',
|
|
418
|
+
description: 'Find the cheapest/fastest path to move money across Asian exchanges. ' +
|
|
419
|
+
'Example: KRW on Bithumb → USDC on Binance. ' +
|
|
420
|
+
'Supports 5 exchanges (Bithumb, Upbit, Coinone, GoPax, Binance) and 11 bridge coins (incl. KAIA). ' +
|
|
421
|
+
'Paid tool: calls /api/premium/route/find ($0.10) via x402 (requires EVM_PRIVATE_KEY).',
|
|
422
|
+
inputSchema: z.object({
|
|
423
|
+
from: z
|
|
424
|
+
.string()
|
|
425
|
+
.describe('Source in exchange:currency format (e.g. "bithumb:KRW", "upbit:KRW")'),
|
|
426
|
+
to: z
|
|
427
|
+
.string()
|
|
428
|
+
.describe('Destination in exchange:currency format (e.g. "binance:USDC", "binance:BTC")'),
|
|
429
|
+
amount: z.number().describe('Amount in source currency (e.g. 1000000 for ₩1,000,000)'),
|
|
430
|
+
strategy: z
|
|
431
|
+
.enum(['cheapest', 'fastest', 'balanced'])
|
|
432
|
+
.optional()
|
|
433
|
+
.describe('Routing strategy: cheapest (default), fastest, or balanced'),
|
|
434
|
+
}),
|
|
435
|
+
}, async ({ from, to, amount, strategy }) => {
|
|
436
|
+
if (!paidFetch || !httpClient) {
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: 'text', text: 'EVM_PRIVATE_KEY not configured — cannot call paid routing endpoint' }],
|
|
358
439
|
isError: true,
|
|
359
440
|
};
|
|
360
441
|
}
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
return { content: [{ type: 'text', text: 'Provide serviceId or url' }], isError: true };
|
|
364
|
-
}
|
|
365
|
-
if (params && Object.keys(params).length > 0) {
|
|
366
|
-
const qs = new URLSearchParams(params);
|
|
367
|
-
const sep = targetUrl.includes('?') ? '&' : '?';
|
|
368
|
-
targetUrl = `${targetUrl}${sep}${qs.toString()}`;
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
const res = await paidFetch(targetUrl, { method: 'GET' });
|
|
372
|
-
const body = await res.text();
|
|
373
|
-
let data;
|
|
374
442
|
try {
|
|
375
|
-
|
|
443
|
+
const qs = new URLSearchParams({
|
|
444
|
+
from,
|
|
445
|
+
to,
|
|
446
|
+
amount: String(amount),
|
|
447
|
+
});
|
|
448
|
+
if (strategy)
|
|
449
|
+
qs.set('strategy', strategy);
|
|
450
|
+
const targetUrl = ensureCrossfinPaidUrl(`${API_BASE}/api/premium/route/find?${qs.toString()}`);
|
|
451
|
+
const res = await paidFetch(targetUrl, { method: 'GET' });
|
|
452
|
+
const body = await res.text();
|
|
453
|
+
let data;
|
|
454
|
+
try {
|
|
455
|
+
data = JSON.parse(body);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
data = body;
|
|
459
|
+
}
|
|
460
|
+
const settle = httpClient.getPaymentSettleResponse((name) => res.headers.get(name));
|
|
461
|
+
const txHash = settle?.transaction;
|
|
462
|
+
const networkId = settle?.network;
|
|
463
|
+
const scanLink = basescanLink(networkId, txHash);
|
|
464
|
+
const result = {
|
|
465
|
+
status: res.status,
|
|
466
|
+
payer: payerAddress,
|
|
467
|
+
paid: !!settle,
|
|
468
|
+
txHash: txHash ?? null,
|
|
469
|
+
basescan: scanLink,
|
|
470
|
+
data,
|
|
471
|
+
};
|
|
472
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
376
473
|
}
|
|
377
|
-
catch {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
data,
|
|
391
|
-
};
|
|
392
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
393
|
-
}
|
|
394
|
-
catch (e) {
|
|
395
|
-
return {
|
|
396
|
-
content: [{ type: 'text', text: `Payment call failed: ${e instanceof Error ? e.message : String(e)}` }],
|
|
397
|
-
isError: true,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
/* ── Routing Engine Tools ── */
|
|
402
|
-
server.registerTool('find_optimal_route', {
|
|
403
|
-
title: 'Find optimal route',
|
|
404
|
-
description: 'Find the cheapest/fastest path to move money across Asian exchanges. ' +
|
|
405
|
-
'Example: KRW on Bithumb → USDC on Binance. ' +
|
|
406
|
-
'Supports 5 exchanges (Bithumb, Upbit, Coinone, GoPax, Binance) and 12 bridge coins. ' +
|
|
407
|
-
'Paid tool: calls /api/premium/route/find ($0.10) via x402 (requires EVM_PRIVATE_KEY).',
|
|
408
|
-
inputSchema: z.object({
|
|
409
|
-
from: z
|
|
410
|
-
.string()
|
|
411
|
-
.describe('Source in exchange:currency format (e.g. "bithumb:KRW", "upbit:KRW")'),
|
|
412
|
-
to: z
|
|
413
|
-
.string()
|
|
414
|
-
.describe('Destination in exchange:currency format (e.g. "binance:USDC", "binance:BTC")'),
|
|
415
|
-
amount: z.number().describe('Amount in source currency (e.g. 1000000 for ₩1,000,000)'),
|
|
416
|
-
strategy: z
|
|
417
|
-
.enum(['cheapest', 'fastest', 'balanced'])
|
|
418
|
-
.optional()
|
|
419
|
-
.describe('Routing strategy: cheapest (default), fastest, or balanced'),
|
|
420
|
-
}),
|
|
421
|
-
}, async ({ from, to, amount, strategy }) => {
|
|
422
|
-
if (!paidFetch || !httpClient) {
|
|
423
|
-
return {
|
|
424
|
-
content: [{ type: 'text', text: 'EVM_PRIVATE_KEY not configured — cannot call paid routing endpoint' }],
|
|
425
|
-
isError: true,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
try {
|
|
429
|
-
const qs = new URLSearchParams({
|
|
430
|
-
from,
|
|
431
|
-
to,
|
|
432
|
-
amount: String(amount),
|
|
433
|
-
});
|
|
434
|
-
if (strategy)
|
|
435
|
-
qs.set('strategy', strategy);
|
|
436
|
-
const targetUrl = ensureCrossfinPaidUrl(`${API_BASE}/api/premium/route/find?${qs.toString()}`);
|
|
437
|
-
const res = await paidFetch(targetUrl, { method: 'GET' });
|
|
438
|
-
const body = await res.text();
|
|
439
|
-
let data;
|
|
474
|
+
catch (e) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
477
|
+
isError: true,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
server.registerTool('list_exchange_fees', {
|
|
482
|
+
title: 'List exchange fees',
|
|
483
|
+
description: 'Show trading fees, withdrawal fees, and transfer times for all supported exchanges ' +
|
|
484
|
+
'(Bithumb, Upbit, Coinone, GoPax, Binance)',
|
|
485
|
+
inputSchema: z.object({}),
|
|
486
|
+
}, async (_params) => {
|
|
440
487
|
try {
|
|
441
|
-
data =
|
|
488
|
+
const data = await apiFetch('/api/route/fees');
|
|
489
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
442
490
|
}
|
|
443
|
-
catch {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
.string()
|
|
491
|
-
.optional()
|
|
492
|
-
.describe('Coin symbol to compare (e.g. "BTC", "XRP"). Omit for all supported coins.'),
|
|
493
|
-
}),
|
|
494
|
-
}, async ({ coin }) => {
|
|
495
|
-
try {
|
|
496
|
-
const qs = new URLSearchParams();
|
|
497
|
-
if (coin?.trim())
|
|
498
|
-
qs.set('coin', coin.trim().toUpperCase());
|
|
499
|
-
const path = qs.size ? `/api/route/pairs?${qs.toString()}` : '/api/route/pairs';
|
|
500
|
-
const data = await apiFetch(path);
|
|
501
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
502
|
-
}
|
|
503
|
-
catch (e) {
|
|
504
|
-
return {
|
|
505
|
-
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
506
|
-
isError: true,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
const transport = new StdioServerTransport();
|
|
511
|
-
await server.connect(transport);
|
|
491
|
+
catch (e) {
|
|
492
|
+
return {
|
|
493
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
494
|
+
isError: true,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
server.registerTool('compare_exchange_prices', {
|
|
499
|
+
title: 'Compare exchange prices',
|
|
500
|
+
description: 'Compare Bithumb KRW prices vs global USD prices (Binance) for tracked coins. ' +
|
|
501
|
+
'Shows transfer-time estimates and which coins are bridge-capable.',
|
|
502
|
+
inputSchema: z.object({
|
|
503
|
+
coin: z
|
|
504
|
+
.string()
|
|
505
|
+
.optional()
|
|
506
|
+
.describe('Coin symbol to compare (e.g. "BTC", "XRP"). Omit for all supported coins.'),
|
|
507
|
+
}),
|
|
508
|
+
}, async ({ coin }) => {
|
|
509
|
+
try {
|
|
510
|
+
const qs = new URLSearchParams();
|
|
511
|
+
if (coin?.trim())
|
|
512
|
+
qs.set('coin', coin.trim().toUpperCase());
|
|
513
|
+
const path = qs.size ? `/api/route/pairs?${qs.toString()}` : '/api/route/pairs';
|
|
514
|
+
const data = await apiFetch(path);
|
|
515
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
|
|
520
|
+
isError: true,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return server.server;
|
|
525
|
+
}
|
|
526
|
+
/* ── Standalone (npx crossfin-mcp) ── */
|
|
527
|
+
if (process.argv[1] && import.meta.url && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'))) {
|
|
528
|
+
const srv = createServer({
|
|
529
|
+
config: {
|
|
530
|
+
evmPrivateKey: process.env.EVM_PRIVATE_KEY,
|
|
531
|
+
apiUrl: process.env.CROSSFIN_API_URL,
|
|
532
|
+
ledgerPath: process.env.CROSSFIN_LEDGER_PATH,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
const transport = new StdioServerTransport();
|
|
536
|
+
srv.connect(transport).catch((e) => { console.error(e); process.exit(1); });
|
|
537
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crossfin-mcp",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.8.
|
|
4
|
+
"version": "1.8.5",
|
|
5
|
+
"mcpName": "io.github.bubilife1202/crossfin",
|
|
5
6
|
"description": "CrossFin MCP server for service discovery and paid x402 API execution",
|
|
6
7
|
"bin": {
|
|
7
8
|
"crossfin-mcp": "dist/index.js"
|