@routedock/mcp-server 0.2.1 → 0.2.2
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 +16 -5
- package/dist/index.js +344 -34
- package/package.json +23 -3
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Model Context Protocol (MCP) server for RouteDock Stellar payments. Exposes RouteDock's payment functionality as standardized tools that LLM agents can use directly.
|
|
4
4
|
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
Node.js **22 or newer**, matching the supported runtime of all RouteDock npm packages.
|
|
8
|
+
|
|
5
9
|
## Overview
|
|
6
10
|
|
|
7
11
|
This MCP server implements the thesis that base payment infrastructure should be built for machines, not humans. Instead of requiring every LLM agent to implement the full RouteDock SDK, agents can simply connect to this MCP server and use standardized tools:
|
|
@@ -36,7 +40,7 @@ Create a `.env` file (e.g., `~/.routedock/.env`):
|
|
|
36
40
|
```bash
|
|
37
41
|
# Required
|
|
38
42
|
STELLAR_SECRET="SDU5..." # Use a dedicated low-balance testnet key for safety
|
|
39
|
-
STELLAR_NETWORK="testnet" # or "mainnet"
|
|
43
|
+
STELLAR_NETWORK="testnet" # "testnet" (default) or "mainnet" (aliases: "pubnet", "public")
|
|
40
44
|
ROUTEDOCK_DAILY_CAP="1.00" # Required: Maximum daily spend in USDC
|
|
41
45
|
|
|
42
46
|
# Optional (for session mode)
|
|
@@ -56,7 +60,9 @@ Pay for a single data request from a RouteDock provider. Automatically selects t
|
|
|
56
60
|
|
|
57
61
|
**Parameters:**
|
|
58
62
|
- `url` (required): Full URL of the provider endpoint
|
|
59
|
-
- `max_amount` (required): Maximum USDC amount to pay
|
|
63
|
+
- `max_amount` (required): Maximum USDC amount to pay, as a decimal string such as `"0.01"`: a positive
|
|
64
|
+
number with at most 7 decimal places and no currency symbol. Anything else (missing, `"$0.01"`, `"0"`,
|
|
65
|
+
a JSON number) is rejected before the provider is contacted.
|
|
60
66
|
- `preferred_mode` (optional): Preferred payment mode (`x402`, `mpp-charge`, `mpp-session`)
|
|
61
67
|
|
|
62
68
|
**Returns:** Payment result with mode, amount, transaction hash, and response data
|
|
@@ -69,10 +75,15 @@ check against the provider's `min_deposit`, not a fund transfer. Pass the return
|
|
|
69
75
|
`stream_session` and `close_session` — the channel's collateral cannot be settled otherwise.
|
|
70
76
|
|
|
71
77
|
**Parameters:**
|
|
72
|
-
- `url` (required):
|
|
78
|
+
- `url` (required): The provider's streaming endpoint, e.g. `https://api-b.routedock.xyz/stream/orderbook`.
|
|
79
|
+
Vouchers and the closing `DELETE` are sent to this exact URL. A bare origin such as
|
|
80
|
+
`https://api-b.routedock.xyz` is also accepted: the server reads the provider's
|
|
81
|
+
`/.well-known/routedock.json` and uses `endpoints.stream`, or the only endpoint if there is just one.
|
|
82
|
+
The endpoint must be `GET` and on the same origin. If none qualifies, the call fails without opening
|
|
83
|
+
a session and lists the manifest's endpoints so you can retry with a full URL.
|
|
73
84
|
- `initial_deposit` (optional): Amount in USDC you intend the channel to be funded with
|
|
74
85
|
|
|
75
|
-
**Returns:** `channel_id
|
|
86
|
+
**Returns:** `channel_id`, open transaction hash, and `endpoint_url` (the URL the session streams from)
|
|
76
87
|
|
|
77
88
|
### stream_session
|
|
78
89
|
|
|
@@ -145,7 +156,7 @@ Once configured, you can ask Claude to:
|
|
|
145
156
|
> "Check my Stellar balance"
|
|
146
157
|
> "List providers that offer price data"
|
|
147
158
|
> "Pay for data from https://api-a.routedock.xyz/price with a max of 0.01 USDC"
|
|
148
|
-
> "Open a session with https://api-b.routedock.xyz for streaming orderbook data"
|
|
159
|
+
> "Open a session with https://api-b.routedock.xyz/stream/orderbook for streaming orderbook data"
|
|
149
160
|
> "Pull the next few messages from that session"
|
|
150
161
|
> "Close the session and settle the channel"
|
|
151
162
|
|
package/dist/index.js
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { config as loadDotenv } from "dotenv";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import os from "os";
|
|
7
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
7
|
import {
|
|
10
8
|
CallToolRequestSchema,
|
|
11
9
|
ListToolsRequestSchema
|
|
12
10
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
-
import { RouteDockClient
|
|
11
|
+
import { RouteDockClient } from "@routedock/routedock";
|
|
12
|
+
import { FileSpendStore } from "@routedock/routedock/node";
|
|
14
13
|
import { createClient } from "@supabase/supabase-js";
|
|
15
14
|
|
|
16
15
|
// src/tools.ts
|
|
@@ -47,7 +46,7 @@ var TOOLS = [
|
|
|
47
46
|
properties: {
|
|
48
47
|
url: {
|
|
49
48
|
type: "string",
|
|
50
|
-
description: "The
|
|
49
|
+
description: "The provider's streaming endpoint URL (e.g., https://api-b.routedock.xyz/stream/orderbook). Vouchers and the closing DELETE go to this exact URL. A bare origin (e.g., https://api-b.routedock.xyz) is also accepted and resolved to the provider's streaming endpoint from its /.well-known/routedock.json manifest."
|
|
51
50
|
},
|
|
52
51
|
initial_deposit: {
|
|
53
52
|
type: "string",
|
|
@@ -131,6 +130,8 @@ var TOOLS = [
|
|
|
131
130
|
];
|
|
132
131
|
|
|
133
132
|
// src/handlers.ts
|
|
133
|
+
import { usdcToStroops } from "@routedock/routedock";
|
|
134
|
+
import { HORIZON_URLS } from "@routedock/routedock/network";
|
|
134
135
|
import { Keypair, Horizon } from "@stellar/stellar-sdk";
|
|
135
136
|
function ok(payload) {
|
|
136
137
|
return {
|
|
@@ -143,15 +144,30 @@ function err(message) {
|
|
|
143
144
|
isError: true
|
|
144
145
|
};
|
|
145
146
|
}
|
|
147
|
+
function parseUsdc(amount) {
|
|
148
|
+
if (typeof amount !== "string") return null;
|
|
149
|
+
try {
|
|
150
|
+
return usdcToStroops(amount);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
146
155
|
async function handlePayForData(args, deps2) {
|
|
147
156
|
const { url, max_amount, preferred_mode } = args;
|
|
148
157
|
const { client: client2 } = deps2;
|
|
158
|
+
const maxStroops = parseUsdc(max_amount);
|
|
159
|
+
if (maxStroops === null || maxStroops === 0n) {
|
|
160
|
+
return err(
|
|
161
|
+
`max_amount must be a positive USDC amount as a decimal string with at most 7 decimal places, e.g. "0.01" (received ${JSON.stringify(max_amount) ?? "nothing"})`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
149
164
|
const modeOptions = preferred_mode ? { forceMode: preferred_mode } : void 0;
|
|
150
165
|
const estimate = await client2.estimateCost(url, modeOptions);
|
|
151
|
-
|
|
166
|
+
const priceStroops = parseUsdc(estimate.amount);
|
|
167
|
+
if (priceStroops === null) {
|
|
152
168
|
return err("Provider returned an undefined or invalid price");
|
|
153
169
|
}
|
|
154
|
-
if (
|
|
170
|
+
if (priceStroops > maxStroops) {
|
|
155
171
|
return err(
|
|
156
172
|
`Provider cost ${estimate.amount} ${estimate.asset} exceeds max_amount ${max_amount} USDC`
|
|
157
173
|
);
|
|
@@ -166,18 +182,64 @@ async function handlePayForData(args, deps2) {
|
|
|
166
182
|
data: result.data
|
|
167
183
|
});
|
|
168
184
|
}
|
|
185
|
+
function resolveSessionEndpoint(origin, manifest) {
|
|
186
|
+
const raw = manifest?.endpoints;
|
|
187
|
+
const endpoints = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
188
|
+
const entries = Object.entries(endpoints);
|
|
189
|
+
const listing = entries.length > 0 ? entries.map(([name2, e]) => `${name2}: ${String(e?.method)} ${String(e?.path)}`).join(", ") : "(none)";
|
|
190
|
+
const fail = (reason) => ({
|
|
191
|
+
error: `Could not resolve a streaming endpoint for ${origin}: ${reason}. Endpoints in its manifest: ${listing}. Call open_session again with the full endpoint URL (e.g. ${origin}/stream/orderbook).`
|
|
192
|
+
});
|
|
193
|
+
let chosen;
|
|
194
|
+
if (Object.hasOwn(endpoints, "stream")) {
|
|
195
|
+
chosen = ["stream", endpoints.stream];
|
|
196
|
+
} else if (entries.length === 1) {
|
|
197
|
+
chosen = entries[0];
|
|
198
|
+
}
|
|
199
|
+
if (!chosen) {
|
|
200
|
+
return fail(
|
|
201
|
+
entries.length === 0 ? "the manifest lists no endpoints" : `the manifest has no "stream" endpoint and ${entries.length} others to choose from`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const [name, endpoint] = chosen;
|
|
205
|
+
const method = typeof endpoint?.method === "string" ? endpoint.method.toUpperCase() : "";
|
|
206
|
+
if (method !== "GET") {
|
|
207
|
+
return fail(`endpoint "${name}" uses method ${String(endpoint?.method)}, but sessions stream with GET`);
|
|
208
|
+
}
|
|
209
|
+
if (typeof endpoint.path !== "string") {
|
|
210
|
+
return fail(`endpoint "${name}" has no path`);
|
|
211
|
+
}
|
|
212
|
+
let resolved;
|
|
213
|
+
try {
|
|
214
|
+
resolved = new URL(endpoint.path, origin);
|
|
215
|
+
} catch {
|
|
216
|
+
return fail(`endpoint "${name}" has an invalid path`);
|
|
217
|
+
}
|
|
218
|
+
if (resolved.origin !== origin) {
|
|
219
|
+
return fail(`endpoint "${name}" points outside ${origin}`);
|
|
220
|
+
}
|
|
221
|
+
return { url: resolved.href };
|
|
222
|
+
}
|
|
169
223
|
async function handleOpenSession(args, deps2, commitmentSecret) {
|
|
170
224
|
const { url, initial_deposit } = args;
|
|
171
225
|
const { client: client2, openSessions: openSessions2, fetchManifest } = deps2;
|
|
172
226
|
if (!commitmentSecret) {
|
|
173
227
|
return err("COMMITMENT_SECRET environment variable is required for session mode");
|
|
174
228
|
}
|
|
229
|
+
const parsed = new URL(url);
|
|
230
|
+
const origin = parsed.origin;
|
|
231
|
+
const isBareOrigin = parsed.pathname === "" || parsed.pathname === "/";
|
|
232
|
+
let cachedManifest;
|
|
233
|
+
const loadManifest = async () => {
|
|
234
|
+
if (cachedManifest === void 0) {
|
|
235
|
+
const fetcher = fetchManifest ?? ((u) => fetch(u));
|
|
236
|
+
const manifestResponse = await fetcher(`${origin}/.well-known/routedock.json`);
|
|
237
|
+
cachedManifest = await manifestResponse.json();
|
|
238
|
+
}
|
|
239
|
+
return cachedManifest;
|
|
240
|
+
};
|
|
175
241
|
if (initial_deposit) {
|
|
176
|
-
const
|
|
177
|
-
const manifestUrl = `${baseUrl}/.well-known/routedock.json`;
|
|
178
|
-
const fetcher = fetchManifest ?? ((u) => fetch(u));
|
|
179
|
-
const manifestResponse = await fetcher(manifestUrl);
|
|
180
|
-
const manifest = await manifestResponse.json();
|
|
242
|
+
const manifest = await loadManifest();
|
|
181
243
|
const minDeposit = manifest?.pricing?.["mpp-session"]?.min_deposit ?? manifest?.pricing?.["mpp-session-ws"]?.min_deposit;
|
|
182
244
|
if (minDeposit && parseFloat(initial_deposit) < parseFloat(minDeposit)) {
|
|
183
245
|
return err(
|
|
@@ -185,12 +247,21 @@ async function handleOpenSession(args, deps2, commitmentSecret) {
|
|
|
185
247
|
);
|
|
186
248
|
}
|
|
187
249
|
}
|
|
188
|
-
|
|
250
|
+
let endpointUrl = url;
|
|
251
|
+
if (isBareOrigin) {
|
|
252
|
+
const resolution = resolveSessionEndpoint(origin, await loadManifest());
|
|
253
|
+
if ("error" in resolution) {
|
|
254
|
+
return err(resolution.error);
|
|
255
|
+
}
|
|
256
|
+
endpointUrl = resolution.url;
|
|
257
|
+
}
|
|
258
|
+
const session = await client2.openSession(endpointUrl);
|
|
189
259
|
openSessions2.set(session.channelId, session);
|
|
190
260
|
return ok({
|
|
191
261
|
success: true,
|
|
192
262
|
channel_id: session.channelId,
|
|
193
263
|
open_tx_hash: session.openTxHash,
|
|
264
|
+
endpoint_url: endpointUrl,
|
|
194
265
|
message: "Session opened successfully. Use stream_session to pull data and close_session to settle and release the channel collateral."
|
|
195
266
|
});
|
|
196
267
|
}
|
|
@@ -267,10 +338,6 @@ async function handleCloseSession(args, deps2) {
|
|
|
267
338
|
vouchers_issued: result.vouchersIssued
|
|
268
339
|
});
|
|
269
340
|
}
|
|
270
|
-
var HORIZON_URLS = {
|
|
271
|
-
mainnet: "https://horizon.stellar.org",
|
|
272
|
-
testnet: "https://horizon-testnet.stellar.org"
|
|
273
|
-
};
|
|
274
341
|
function normalizeBalances(balances) {
|
|
275
342
|
return balances.map((b) => ({
|
|
276
343
|
asset_type: b.asset_type,
|
|
@@ -351,6 +418,249 @@ async function handleListProviders(args, deps2) {
|
|
|
351
418
|
});
|
|
352
419
|
}
|
|
353
420
|
|
|
421
|
+
// src/env.ts
|
|
422
|
+
import path from "path";
|
|
423
|
+
import os from "os";
|
|
424
|
+
|
|
425
|
+
// package.json
|
|
426
|
+
var package_default = {
|
|
427
|
+
name: "@routedock/mcp-server",
|
|
428
|
+
version: "0.2.2",
|
|
429
|
+
description: "MCP server for RouteDock Stellar payments - exposes payment tools to LLM agents",
|
|
430
|
+
license: "MIT",
|
|
431
|
+
homepage: "https://github.com/winsznx/routedock/tree/main/packages/mcp-server#readme",
|
|
432
|
+
bugs: {
|
|
433
|
+
url: "https://github.com/winsznx/routedock/issues"
|
|
434
|
+
},
|
|
435
|
+
publishConfig: {
|
|
436
|
+
provenance: true
|
|
437
|
+
},
|
|
438
|
+
repository: {
|
|
439
|
+
type: "git",
|
|
440
|
+
url: "https://github.com/winsznx/routedock.git",
|
|
441
|
+
directory: "packages/mcp-server"
|
|
442
|
+
},
|
|
443
|
+
type: "module",
|
|
444
|
+
main: "dist/index.js",
|
|
445
|
+
types: "dist/index.d.ts",
|
|
446
|
+
files: [
|
|
447
|
+
"dist",
|
|
448
|
+
"README.md"
|
|
449
|
+
],
|
|
450
|
+
bin: {
|
|
451
|
+
"routedock-mcp-server": "dist/index.js"
|
|
452
|
+
},
|
|
453
|
+
scripts: {
|
|
454
|
+
build: "tsup",
|
|
455
|
+
dev: "tsup --watch",
|
|
456
|
+
typecheck: "tsc --noEmit",
|
|
457
|
+
test: 'tsx --experimental-test-module-mocks --test "src/**/*.test.ts"',
|
|
458
|
+
start: "node dist/index.js"
|
|
459
|
+
},
|
|
460
|
+
dependencies: {
|
|
461
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
462
|
+
"@routedock/routedock": "workspace:^",
|
|
463
|
+
"@stellar/stellar-sdk": "^14.6.1",
|
|
464
|
+
"@supabase/supabase-js": "^2.47.0",
|
|
465
|
+
dotenv: "^17.4.2"
|
|
466
|
+
},
|
|
467
|
+
devDependencies: {
|
|
468
|
+
"@types/node": "^20.0.0",
|
|
469
|
+
tsup: "^8.0.0",
|
|
470
|
+
tsx: "^4.22.3",
|
|
471
|
+
typescript: "^5.4.5"
|
|
472
|
+
},
|
|
473
|
+
engines: {
|
|
474
|
+
node: ">=22"
|
|
475
|
+
},
|
|
476
|
+
keywords: [
|
|
477
|
+
"stellar",
|
|
478
|
+
"soroban",
|
|
479
|
+
"usdc",
|
|
480
|
+
"x402",
|
|
481
|
+
"mpp",
|
|
482
|
+
"payments",
|
|
483
|
+
"ai-agents",
|
|
484
|
+
"mcp"
|
|
485
|
+
]
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// src/env.ts
|
|
489
|
+
var SERVER_NAME = "@routedock/mcp-server";
|
|
490
|
+
var SERVER_VERSION = package_default.version;
|
|
491
|
+
function validateStellarNetwork(networkInput, warn = console.warn) {
|
|
492
|
+
if (!networkInput || networkInput.trim() === "") {
|
|
493
|
+
return "testnet";
|
|
494
|
+
}
|
|
495
|
+
const normalized = networkInput.trim().toLowerCase();
|
|
496
|
+
if (normalized === "testnet") {
|
|
497
|
+
return "testnet";
|
|
498
|
+
}
|
|
499
|
+
if (normalized === "mainnet") {
|
|
500
|
+
return "mainnet";
|
|
501
|
+
}
|
|
502
|
+
if (normalized === "pubnet" || normalized === "public") {
|
|
503
|
+
warn(`WARNING: STELLAR_NETWORK='${networkInput}' is an alias for 'mainnet'. Using 'mainnet'.`);
|
|
504
|
+
return "mainnet";
|
|
505
|
+
}
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Invalid STELLAR_NETWORK "${networkInput}". Allowed values: testnet, mainnet (or pubnet/public as alias for mainnet).`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
function parseEnv(env = process.env, options) {
|
|
511
|
+
const warn = options?.warn ?? console.warn;
|
|
512
|
+
if (env.SUPABASE_SERVICE_KEY) {
|
|
513
|
+
warn(
|
|
514
|
+
"WARNING: Using SUPABASE_SERVICE_KEY bypasses RLS. Use an anon key for list_providers (anon + public_read_providers RLS is sufficient)."
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const stellarSecret = env.STELLAR_SECRET || env.ROUTEDOCK_WALLET_SECRET;
|
|
518
|
+
if (!stellarSecret) {
|
|
519
|
+
throw new Error("STELLAR_SECRET or ROUTEDOCK_WALLET_SECRET environment variable is required");
|
|
520
|
+
}
|
|
521
|
+
const routedockDailyCap = env.ROUTEDOCK_DAILY_CAP;
|
|
522
|
+
if (!routedockDailyCap) {
|
|
523
|
+
throw new Error("ROUTEDOCK_DAILY_CAP environment variable is required to prevent unbounded spending");
|
|
524
|
+
}
|
|
525
|
+
const stellarNetwork = validateStellarNetwork(env.STELLAR_NETWORK, warn);
|
|
526
|
+
const commitmentSecret = env.COMMITMENT_SECRET;
|
|
527
|
+
const supabaseUrl = env.SUPABASE_URL;
|
|
528
|
+
const supabaseKey = env.SUPABASE_KEY || env.SUPABASE_SERVICE_KEY;
|
|
529
|
+
const spendStorePath2 = env.ROUTEDOCK_SPEND_STORE_PATH || path.join(os.homedir(), ".routedock", "spend.json");
|
|
530
|
+
return {
|
|
531
|
+
stellarSecret,
|
|
532
|
+
stellarNetwork,
|
|
533
|
+
commitmentSecret,
|
|
534
|
+
supabaseUrl,
|
|
535
|
+
supabaseKey,
|
|
536
|
+
routedockDailyCap,
|
|
537
|
+
spendStorePath: spendStorePath2
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/shutdown.ts
|
|
542
|
+
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
543
|
+
async function closeAllSessions(openSessions2, options = {}) {
|
|
544
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
545
|
+
const logError = options.logError ?? console.error;
|
|
546
|
+
const logInfo = options.logInfo ?? console.error;
|
|
547
|
+
const entries = Array.from(openSessions2.entries());
|
|
548
|
+
if (entries.length === 0) {
|
|
549
|
+
return { closed: 0, failed: 0, timedOut: 0, results: [] };
|
|
550
|
+
}
|
|
551
|
+
logInfo(`[mcp-server] Closing ${entries.length} active session(s)...`);
|
|
552
|
+
const trackers = entries.map(([channelId, session]) => ({
|
|
553
|
+
channelId,
|
|
554
|
+
session,
|
|
555
|
+
status: "pending",
|
|
556
|
+
error: void 0
|
|
557
|
+
}));
|
|
558
|
+
const closePromises = trackers.map(async (tracker) => {
|
|
559
|
+
try {
|
|
560
|
+
await tracker.session.close();
|
|
561
|
+
tracker.status = "closed";
|
|
562
|
+
openSessions2.delete(tracker.channelId);
|
|
563
|
+
logInfo(`[mcp-server] Successfully closed session ${tracker.channelId}`);
|
|
564
|
+
} catch (err2) {
|
|
565
|
+
tracker.status = "error";
|
|
566
|
+
tracker.error = err2;
|
|
567
|
+
openSessions2.delete(tracker.channelId);
|
|
568
|
+
logError(`[mcp-server] Failed to close session ${tracker.channelId}:`, err2);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
let timer;
|
|
572
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
573
|
+
timer = setTimeout(() => resolve("TIMEOUT"), timeoutMs);
|
|
574
|
+
});
|
|
575
|
+
const allFinishedPromise = Promise.all(closePromises).then(() => "DONE");
|
|
576
|
+
const outcome = await Promise.race([allFinishedPromise, timeoutPromise]);
|
|
577
|
+
if (timer) {
|
|
578
|
+
clearTimeout(timer);
|
|
579
|
+
}
|
|
580
|
+
if (outcome === "TIMEOUT") {
|
|
581
|
+
for (const tracker of trackers) {
|
|
582
|
+
if (tracker.status === "pending") {
|
|
583
|
+
tracker.status = "timeout";
|
|
584
|
+
openSessions2.delete(tracker.channelId);
|
|
585
|
+
logError(
|
|
586
|
+
`[mcp-server] Timed out waiting to close session ${tracker.channelId} after ${timeoutMs}ms`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const results = trackers.map((t) => {
|
|
592
|
+
const status = t.status === "pending" ? "timeout" : t.status;
|
|
593
|
+
return {
|
|
594
|
+
channelId: t.channelId,
|
|
595
|
+
status,
|
|
596
|
+
error: t.error
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
const closed = results.filter((r) => r.status === "closed").length;
|
|
600
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
601
|
+
const timedOut = results.filter((r) => r.status === "timeout").length;
|
|
602
|
+
return { closed, failed, timedOut, results };
|
|
603
|
+
}
|
|
604
|
+
function setupShutdownHandlers(options) {
|
|
605
|
+
const {
|
|
606
|
+
openSessions: openSessions2,
|
|
607
|
+
transport,
|
|
608
|
+
stdin = process.stdin,
|
|
609
|
+
processObj = process,
|
|
610
|
+
timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
611
|
+
logError = console.error,
|
|
612
|
+
logInfo = console.error,
|
|
613
|
+
onExit = (code) => {
|
|
614
|
+
process.exit(code);
|
|
615
|
+
}
|
|
616
|
+
} = options;
|
|
617
|
+
let shutdownPromise = null;
|
|
618
|
+
const handleShutdown = (reason) => {
|
|
619
|
+
if (shutdownPromise) {
|
|
620
|
+
return shutdownPromise;
|
|
621
|
+
}
|
|
622
|
+
logInfo(`[mcp-server] Graceful shutdown initiated (${reason})`);
|
|
623
|
+
shutdownPromise = (async () => {
|
|
624
|
+
try {
|
|
625
|
+
const summary = await closeAllSessions(openSessions2, {
|
|
626
|
+
timeoutMs,
|
|
627
|
+
logError,
|
|
628
|
+
logInfo
|
|
629
|
+
});
|
|
630
|
+
const exitCode = summary.failed > 0 || summary.timedOut > 0 ? 1 : 0;
|
|
631
|
+
onExit(exitCode);
|
|
632
|
+
return summary;
|
|
633
|
+
} catch (err2) {
|
|
634
|
+
logError("[mcp-server] Unexpected error during shutdown:", err2);
|
|
635
|
+
onExit(1);
|
|
636
|
+
throw err2;
|
|
637
|
+
}
|
|
638
|
+
})();
|
|
639
|
+
return shutdownPromise;
|
|
640
|
+
};
|
|
641
|
+
processObj.once("SIGINT", () => {
|
|
642
|
+
void handleShutdown("SIGINT");
|
|
643
|
+
});
|
|
644
|
+
processObj.once("SIGTERM", () => {
|
|
645
|
+
void handleShutdown("SIGTERM");
|
|
646
|
+
});
|
|
647
|
+
if (stdin && typeof stdin.once === "function") {
|
|
648
|
+
stdin.once("end", () => {
|
|
649
|
+
void handleShutdown("stdin end");
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (transport) {
|
|
653
|
+
const prevOnClose = transport.onclose;
|
|
654
|
+
transport.onclose = () => {
|
|
655
|
+
if (typeof prevOnClose === "function") {
|
|
656
|
+
prevOnClose();
|
|
657
|
+
}
|
|
658
|
+
void handleShutdown("transport onclose");
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
return (reason = "manual") => handleShutdown(reason);
|
|
662
|
+
}
|
|
663
|
+
|
|
354
664
|
// src/index.ts
|
|
355
665
|
var envPath = process.env.ROUTEDOCK_ENV_FILE;
|
|
356
666
|
if (envPath) {
|
|
@@ -358,24 +668,23 @@ if (envPath) {
|
|
|
358
668
|
} else {
|
|
359
669
|
loadDotenv();
|
|
360
670
|
}
|
|
361
|
-
var
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
console.warn("WARNING: Using SUPABASE_SERVICE_KEY bypasses RLS. Use an anon key for list_providers (anon + public_read_providers RLS is sufficient).");
|
|
368
|
-
}
|
|
369
|
-
if (!STELLAR_SECRET) {
|
|
370
|
-
console.error("Error: STELLAR_SECRET or ROUTEDOCK_WALLET_SECRET environment variable is required");
|
|
671
|
+
var config;
|
|
672
|
+
try {
|
|
673
|
+
config = parseEnv(process.env);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
676
|
+
console.error(`Error: ${message}`);
|
|
371
677
|
process.exit(1);
|
|
372
678
|
}
|
|
373
|
-
var
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
679
|
+
var {
|
|
680
|
+
stellarSecret: STELLAR_SECRET,
|
|
681
|
+
stellarNetwork: STELLAR_NETWORK,
|
|
682
|
+
commitmentSecret: COMMITMENT_SECRET,
|
|
683
|
+
supabaseUrl: SUPABASE_URL,
|
|
684
|
+
supabaseKey: SUPABASE_KEY,
|
|
685
|
+
routedockDailyCap: ROUTEDOCK_DAILY_CAP,
|
|
686
|
+
spendStorePath
|
|
687
|
+
} = config;
|
|
379
688
|
var client = new RouteDockClient({
|
|
380
689
|
wallet: STELLAR_SECRET,
|
|
381
690
|
network: STELLAR_NETWORK,
|
|
@@ -397,8 +706,8 @@ var deps = {
|
|
|
397
706
|
};
|
|
398
707
|
var server = new Server(
|
|
399
708
|
{
|
|
400
|
-
name:
|
|
401
|
-
version:
|
|
709
|
+
name: SERVER_NAME,
|
|
710
|
+
version: SERVER_VERSION
|
|
402
711
|
},
|
|
403
712
|
{
|
|
404
713
|
capabilities: {
|
|
@@ -445,6 +754,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
445
754
|
});
|
|
446
755
|
async function main() {
|
|
447
756
|
const transport = new StdioServerTransport();
|
|
757
|
+
setupShutdownHandlers({ openSessions, transport });
|
|
448
758
|
await server.connect(transport);
|
|
449
759
|
console.error("@routedock/mcp-server running on stdio");
|
|
450
760
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@routedock/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "MCP server for RouteDock Stellar payments - exposes payment tools to LLM agents",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/winsznx/routedock/tree/main/packages/mcp-server#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/winsznx/routedock/issues"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"provenance": true
|
|
12
|
+
},
|
|
6
13
|
"repository": {
|
|
7
14
|
"type": "git",
|
|
8
15
|
"url": "https://github.com/winsznx/routedock.git",
|
|
@@ -19,11 +26,11 @@
|
|
|
19
26
|
"routedock-mcp-server": "dist/index.js"
|
|
20
27
|
},
|
|
21
28
|
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
23
30
|
"@stellar/stellar-sdk": "^14.6.1",
|
|
24
31
|
"@supabase/supabase-js": "^2.47.0",
|
|
25
32
|
"dotenv": "^17.4.2",
|
|
26
|
-
"@routedock/routedock": "^0.
|
|
33
|
+
"@routedock/routedock": "^0.4.0"
|
|
27
34
|
},
|
|
28
35
|
"devDependencies": {
|
|
29
36
|
"@types/node": "^20.0.0",
|
|
@@ -31,6 +38,19 @@
|
|
|
31
38
|
"tsx": "^4.22.3",
|
|
32
39
|
"typescript": "^5.4.5"
|
|
33
40
|
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"stellar",
|
|
46
|
+
"soroban",
|
|
47
|
+
"usdc",
|
|
48
|
+
"x402",
|
|
49
|
+
"mpp",
|
|
50
|
+
"payments",
|
|
51
|
+
"ai-agents",
|
|
52
|
+
"mcp"
|
|
53
|
+
],
|
|
34
54
|
"scripts": {
|
|
35
55
|
"build": "tsup",
|
|
36
56
|
"dev": "tsup --watch",
|