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.
Files changed (3) hide show
  1. package/README.md +52 -96
  2. package/dist/index.js +490 -464
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,85 +1,14 @@
1
1
  # CrossFin MCP Server
2
2
 
3
- MCP server for CrossFin 16 tools for service discovery, local ledger, routing engine, and paid API execution.
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 (npm)
6
-
7
- Use directly with `npx`:
5
+ ## Install
8
6
 
9
7
  ```bash
10
8
  npx -y crossfin-mcp
11
9
  ```
12
10
 
13
- Or install globally:
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
- Or point Claude Desktop to the local built output:
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
- ```json
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
- Build:
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
- ```bash
120
- cd apps/mcp-server
121
- npm run build
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
- const LEDGER_PATH = process.env.CROSSFIN_LEDGER_PATH?.trim() || defaultLedgerPath();
11
- const API_BASE = (process.env.CROSSFIN_API_URL?.trim() || 'https://crossfin.dev').replace(/\/$/, '');
12
- const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY?.trim() ?? '';
13
- const API_ORIGIN = new URL(API_BASE).origin;
14
- const require = createRequire(import.meta.url);
15
- const pkg = require('../package.json');
16
- const MCP_VERSION = typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : '0.0.0';
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
- function basescanLink(networkId, txHash) {
52
- if (!txHash)
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
- return res.json();
68
- }
69
- server.registerTool('create_wallet', {
70
- title: 'Create wallet',
71
- description: 'Create a wallet in the local CrossFin ledger',
72
- inputSchema: z.object({
73
- label: z.string().min(1).describe('Wallet label'),
74
- initialDepositKrw: z.number().optional().describe('Optional initial deposit (KRW)'),
75
- }),
76
- outputSchema: z.object({
77
- walletId: z.string(),
78
- label: z.string(),
79
- balanceKrw: z.number(),
80
- }),
81
- }, async ({ label, initialDepositKrw }) => {
82
- const wallet = await createWallet(LEDGER_PATH, label, initialDepositKrw ?? 0);
83
- const out = { walletId: wallet.id, label: wallet.label, balanceKrw: wallet.balanceKrw };
84
- return {
85
- content: [{ type: 'text', text: JSON.stringify(out) }],
86
- structuredContent: out,
87
- };
88
- });
89
- server.registerTool('get_balance', {
90
- title: 'Get balance',
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: `Wallet not found: ${walletId}` }],
99
- structuredContent: { walletId, balanceKrw: -1 },
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
- if (!result) {
132
- return {
133
- content: [{ type: 'text', text: 'Transfer failed (check wallet ids / balance / amount)' }],
134
- structuredContent: {
135
- transactionId: '',
136
- fromBalanceKrw: -1,
137
- toBalanceKrw: -1,
138
- },
139
- };
140
- }
141
- const out = {
142
- transactionId: result.tx.id,
143
- fromBalanceKrw: result.fromBalanceKrw,
144
- toBalanceKrw: result.toBalanceKrw,
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: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
256
- isError: true,
118
+ content: [{ type: 'text', text: JSON.stringify(out) }],
119
+ structuredContent: out,
257
120
  };
258
- }
259
- });
260
- server.registerTool('get_kimchi_premium', {
261
- title: 'Get kimchi premium',
262
- description: 'Get free preview of the Kimchi Premium index — real-time price spread between Korean and global crypto exchanges (top 3 pairs)',
263
- inputSchema: z.object({}),
264
- }, async (_params) => {
265
- try {
266
- const data = await apiFetch('/api/arbitrage/demo');
267
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
268
- }
269
- catch (e) {
270
- return {
271
- content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
272
- isError: true,
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: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
288
- isError: true,
161
+ content: [{ type: 'text', text: JSON.stringify(out) }],
162
+ structuredContent: out,
289
163
  };
290
- }
291
- });
292
- server.registerTool('get_guide', {
293
- title: 'Get CrossFin guide',
294
- description: 'Get the complete CrossFin API guide — what services are available, how to search, pricing, x402 payment flow, and code examples',
295
- inputSchema: z.object({}),
296
- }, async (_params) => {
297
- try {
298
- const data = await apiFetch('/api/docs/guide');
299
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
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: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
304
- isError: true,
176
+ content: [{ type: 'text', text: JSON.stringify({ transactions: txs }) }],
177
+ structuredContent: { transactions: txs },
305
178
  };
306
- }
307
- });
308
- server.registerTool('call_paid_service', {
309
- title: 'Call paid service',
310
- description: 'Call a CrossFin paid API endpoint with automatic x402 USDC payment on Base. ' +
311
- 'Requires EVM_PRIVATE_KEY env var with funded wallet. ' +
312
- 'Returns the API response data plus payment proof (txHash, basescan link).',
313
- inputSchema: z.object({
314
- serviceId: z.string().optional().describe('Service ID from registry — looks up endpoint/price automatically'),
315
- url: z.string().optional().describe('Direct URL to call (use this OR serviceId, not both)'),
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: 'EVM_PRIVATE_KEY not configured — cannot make paid calls' }],
322
- isError: true,
190
+ content: [{ type: 'text', text: JSON.stringify(out) }],
191
+ structuredContent: out,
323
192
  };
324
- }
325
- if (serviceId && url) {
326
- return { content: [{ type: 'text', text: 'Provide serviceId or url, not both' }], isError: true };
327
- }
328
- let targetUrl;
329
- if (url) {
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
- targetUrl = ensureCrossfinPaidUrl(url);
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: `Invalid url: ${e instanceof Error ? e.message : String(e)}` }],
208
+ content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
336
209
  isError: true,
337
210
  };
338
211
  }
339
- }
340
- else if (serviceId) {
341
- if (!serviceId.startsWith('crossfin_')) {
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: 'Only crossfin_* serviceId values are allowed for paid calls' }],
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 svc = await apiFetch(`/api/registry/${encodeURIComponent(serviceId)}`);
349
- const endpoint = typeof svc.data?.endpoint === 'string' ? svc.data.endpoint : '';
350
- if (!endpoint) {
351
- return { content: [{ type: 'text', text: `Service ${serviceId} has no endpoint` }], isError: true };
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
- targetUrl = ensureCrossfinPaidUrl(endpoint);
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: `Registry lookup failed: ${e instanceof Error ? e.message : String(e)}` }],
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
- data = JSON.parse(body);
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
- data = body;
379
- }
380
- const settle = httpClient.getPaymentSettleResponse((name) => res.headers.get(name));
381
- const txHash = settle?.transaction;
382
- const networkId = settle?.network;
383
- const scanLink = basescanLink(networkId, txHash);
384
- const result = {
385
- status: res.status,
386
- payer: payerAddress,
387
- paid: !!settle,
388
- txHash: txHash ?? null,
389
- basescan: scanLink,
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 = JSON.parse(body);
488
+ const data = await apiFetch('/api/route/fees');
489
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
442
490
  }
443
- catch {
444
- data = body;
445
- }
446
- const settle = httpClient.getPaymentSettleResponse((name) => res.headers.get(name));
447
- const txHash = settle?.transaction;
448
- const networkId = settle?.network;
449
- const scanLink = basescanLink(networkId, txHash);
450
- const result = {
451
- status: res.status,
452
- payer: payerAddress,
453
- paid: !!settle,
454
- txHash: txHash ?? null,
455
- basescan: scanLink,
456
- data,
457
- };
458
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
459
- }
460
- catch (e) {
461
- return {
462
- content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
463
- isError: true,
464
- };
465
- }
466
- });
467
- server.registerTool('list_exchange_fees', {
468
- title: 'List exchange fees',
469
- description: 'Show trading fees, withdrawal fees, and transfer times for all supported exchanges ' +
470
- '(Bithumb, Upbit, Coinone, GoPax, Binance)',
471
- inputSchema: z.object({}),
472
- }, async (_params) => {
473
- try {
474
- const data = await apiFetch('/api/route/fees');
475
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
476
- }
477
- catch (e) {
478
- return {
479
- content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : 'Unknown error'}` }],
480
- isError: true,
481
- };
482
- }
483
- });
484
- server.registerTool('compare_exchange_prices', {
485
- title: 'Compare exchange prices',
486
- description: 'Compare Bithumb KRW prices vs global USD prices (Binance) for tracked coins. ' +
487
- 'Shows transfer-time estimates and which coins are bridge-capable.',
488
- inputSchema: z.object({
489
- coin: z
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.3",
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"