@routedock/mcp-server 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +454 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RouteDock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @routedock/mcp-server
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
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:
|
|
8
|
+
|
|
9
|
+
- `pay_for_data(url, max_amount)` - Pay for a single data request
|
|
10
|
+
- `open_session(url)` - Open a sustained MPP session for streaming
|
|
11
|
+
- `stream_session(channel_id)` - Pull streamed data from an open session
|
|
12
|
+
- `close_session(channel_id)` - Settle and close an open session
|
|
13
|
+
- `check_balance()` - Check wallet balance
|
|
14
|
+
- `list_providers(tags)` - Discover available providers
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @routedock/mcp-server
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or build from source:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd packages/mcp-server
|
|
26
|
+
pnpm install
|
|
27
|
+
pnpm build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
For security, it is highly recommended to store your secrets in an external `.env` file rather than inline in the Claude Desktop configuration.
|
|
33
|
+
|
|
34
|
+
Create a `.env` file (e.g., `~/.routedock/.env`):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Required
|
|
38
|
+
STELLAR_SECRET="SDU5..." # Use a dedicated low-balance testnet key for safety
|
|
39
|
+
STELLAR_NETWORK="testnet" # or "mainnet"
|
|
40
|
+
ROUTEDOCK_DAILY_CAP="1.00" # Required: Maximum daily spend in USDC
|
|
41
|
+
|
|
42
|
+
# Optional (for session mode)
|
|
43
|
+
COMMITMENT_SECRET="S..." # Ed25519 secret for channel commitments
|
|
44
|
+
|
|
45
|
+
# Optional (for provider registry)
|
|
46
|
+
SUPABASE_URL="https://..."
|
|
47
|
+
# Note: Use the anon key (anon + public_read_providers RLS is sufficient). Do NOT use the service-role key.
|
|
48
|
+
SUPABASE_KEY="..."
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Available Tools
|
|
52
|
+
|
|
53
|
+
### pay_for_data
|
|
54
|
+
|
|
55
|
+
Pay for a single data request from a RouteDock provider. Automatically selects the best payment mode.
|
|
56
|
+
|
|
57
|
+
**Parameters:**
|
|
58
|
+
- `url` (required): Full URL of the provider endpoint
|
|
59
|
+
- `max_amount` (required): Maximum USDC amount to pay
|
|
60
|
+
- `preferred_mode` (optional): Preferred payment mode (`x402`, `mpp-charge`, `mpp-session`)
|
|
61
|
+
|
|
62
|
+
**Returns:** Payment result with mode, amount, transaction hash, and response data
|
|
63
|
+
|
|
64
|
+
### open_session
|
|
65
|
+
|
|
66
|
+
Open a sustained MPP session for streaming data. Requires `COMMITMENT_SECRET`. RouteDock channels
|
|
67
|
+
are pre-deployed and funded out-of-band before the agent runs, so `initial_deposit` is a safety
|
|
68
|
+
check against the provider's `min_deposit`, not a fund transfer. Pass the returned `channel_id` to
|
|
69
|
+
`stream_session` and `close_session` — the channel's collateral cannot be settled otherwise.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
- `url` (required): Base URL of the provider
|
|
73
|
+
- `initial_deposit` (optional): Amount in USDC you intend the channel to be funded with
|
|
74
|
+
|
|
75
|
+
**Returns:** `channel_id` and open transaction hash
|
|
76
|
+
|
|
77
|
+
### stream_session
|
|
78
|
+
|
|
79
|
+
Pull the next batch of streamed responses from a session opened with `open_session`. Each message
|
|
80
|
+
sends a voucher and waits for the provider's response.
|
|
81
|
+
|
|
82
|
+
**Parameters:**
|
|
83
|
+
- `channel_id` (required): The `channel_id` returned by `open_session`
|
|
84
|
+
- `max_messages` (optional): Maximum number of messages to pull in this call (1 to 50, default 1)
|
|
85
|
+
|
|
86
|
+
**Returns:** The pulled messages. If a voucher fails partway through the batch (local spend cap
|
|
87
|
+
reached, provider 4xx/5xx), the call still returns `isError: true` with the messages that were
|
|
88
|
+
already paid for, plus `count` and `stats`, instead of discarding the batch.
|
|
89
|
+
|
|
90
|
+
### close_session
|
|
91
|
+
|
|
92
|
+
Close a session opened with `open_session`, settling the channel on-chain with the highest signed
|
|
93
|
+
voucher. Required to release the session's locked collateral.
|
|
94
|
+
|
|
95
|
+
**Parameters:**
|
|
96
|
+
- `channel_id` (required): The `channel_id` returned by `open_session`
|
|
97
|
+
|
|
98
|
+
**Returns:** Close transaction hash, total amount paid, and vouchers issued
|
|
99
|
+
|
|
100
|
+
### check_balance
|
|
101
|
+
|
|
102
|
+
Check the Stellar wallet balance for the configured account.
|
|
103
|
+
|
|
104
|
+
**Parameters:**
|
|
105
|
+
- `asset_code` (optional): Asset code to check (e.g., "USDC")
|
|
106
|
+
- `asset_issuer` (optional): Asset issuer address for non-native assets
|
|
107
|
+
|
|
108
|
+
**Returns:** Balance information for the specified asset
|
|
109
|
+
|
|
110
|
+
### list_providers
|
|
111
|
+
|
|
112
|
+
List available RouteDock providers from the registry. Requires Supabase credentials.
|
|
113
|
+
|
|
114
|
+
**Parameters:**
|
|
115
|
+
- `tags` (optional): Comma-separated tags for filtering (e.g., "price,stellar,dex")
|
|
116
|
+
- `network` (optional): Filter by network (`testnet` or `mainnet`)
|
|
117
|
+
|
|
118
|
+
**Returns:** List of matching providers with their capabilities
|
|
119
|
+
|
|
120
|
+
## Claude Desktop Configuration
|
|
121
|
+
|
|
122
|
+
Add this to your Claude Desktop config file:
|
|
123
|
+
|
|
124
|
+
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
125
|
+
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"routedock": {
|
|
131
|
+
"command": "node",
|
|
132
|
+
"args": ["/path/to/@routedock/mcp-server/dist/index.js"],
|
|
133
|
+
"env": {
|
|
134
|
+
"ROUTEDOCK_ENV_FILE": "/absolute/path/to/your/.env"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Example Usage
|
|
142
|
+
|
|
143
|
+
Once configured, you can ask Claude to:
|
|
144
|
+
|
|
145
|
+
> "Check my Stellar balance"
|
|
146
|
+
> "List providers that offer price data"
|
|
147
|
+
> "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"
|
|
149
|
+
> "Pull the next few messages from that session"
|
|
150
|
+
> "Close the session and settle the channel"
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Install dependencies
|
|
156
|
+
pnpm install
|
|
157
|
+
|
|
158
|
+
# Build
|
|
159
|
+
pnpm build
|
|
160
|
+
|
|
161
|
+
# Watch mode
|
|
162
|
+
pnpm dev
|
|
163
|
+
|
|
164
|
+
# Type check
|
|
165
|
+
pnpm typecheck
|
|
166
|
+
|
|
167
|
+
# Run directly
|
|
168
|
+
pnpm start
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Architecture
|
|
172
|
+
|
|
173
|
+
The MCP server wraps the existing `@routedock/routedock` SDK and exposes its functionality through the Model Context Protocol. This allows any MCP-compatible LLM client (Claude Desktop, etc.) to interact with Stellar payments without implementing the SDK directly.
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
LLM Agent → MCP Protocol → @routedock/mcp-server → @routedock/routedock SDK → Stellar Network
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { config as loadDotenv } from "dotenv";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
ListToolsRequestSchema
|
|
12
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import { RouteDockClient, FileSpendStore } from "@routedock/routedock";
|
|
14
|
+
import { createClient } from "@supabase/supabase-js";
|
|
15
|
+
|
|
16
|
+
// src/tools.ts
|
|
17
|
+
var MAX_STREAM_MESSAGES = 50;
|
|
18
|
+
var TOOLS = [
|
|
19
|
+
{
|
|
20
|
+
name: "pay_for_data",
|
|
21
|
+
description: "Pay for a single data request from a RouteDock provider. Automatically selects the best payment mode (x402, mpp-charge, mpp-session, or mpp-session-ws) based on the provider's manifest. Use mpp-session-ws for WebSocket-transport streaming providers (e.g. provider-b).",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
url: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "The full URL of the provider endpoint to pay for (e.g., https://api-a.routedock.xyz/price)"
|
|
28
|
+
},
|
|
29
|
+
max_amount: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: 'Maximum amount in USDC to pay for this request (e.g., "0.01")'
|
|
32
|
+
},
|
|
33
|
+
preferred_mode: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["x402", "mpp-charge", "mpp-session", "mpp-session-ws"],
|
|
36
|
+
description: "Optional preferred payment mode. If not specified, the best mode is selected automatically. mpp-session-ws opens a WebSocket-transport MPP session instead of the default HTTP/SSE variant."
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ["url", "max_amount"]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "open_session",
|
|
44
|
+
description: "Open a sustained MPP session with a provider for streaming data. Requires COMMITMENT_SECRET to be configured. Returns a channel_id \u2014 pass it to stream_session to pull data and to close_session when done, or the channel collateral can never be settled.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
url: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "The base URL of the provider (e.g., https://api-b.routedock.xyz)"
|
|
51
|
+
},
|
|
52
|
+
initial_deposit: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: `Amount in USDC you intend the channel to be funded with (e.g., "1.0"). RouteDock channels are pre-deployed and funded out-of-band before the agent runs \u2014 this is checked against the provider's advertised min_deposit as a safety guard, it does not itself move funds.`
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
required: ["url"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "stream_session",
|
|
62
|
+
description: "Pull the next batch of streamed responses from a session opened with open_session. Each message sends a voucher and waits for the provider to acknowledge it. Call repeatedly to keep streaming, then call close_session to settle and release the channel.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
channel_id: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "The channel_id returned by open_session"
|
|
69
|
+
},
|
|
70
|
+
max_messages: {
|
|
71
|
+
type: "integer",
|
|
72
|
+
minimum: 1,
|
|
73
|
+
maximum: MAX_STREAM_MESSAGES,
|
|
74
|
+
description: `Maximum number of messages to pull in this call (1 to ${MAX_STREAM_MESSAGES}, default 1)`
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ["channel_id"]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "close_session",
|
|
82
|
+
description: "Close an MPP session opened with open_session, settling the channel on-chain with the highest signed voucher. This is required to release the session's locked collateral \u2014 an open session left unclosed keeps funds locked until the SDK's wall-clock auto-close guard fires.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
channel_id: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "The channel_id returned by open_session"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
required: ["channel_id"]
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "check_balance",
|
|
96
|
+
description: "Check the Stellar wallet balance for the configured account",
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {
|
|
100
|
+
asset_code: {
|
|
101
|
+
type: "string",
|
|
102
|
+
description: 'Optional asset code to check (e.g., "USDC"). If not specified, returns native XLM balance.'
|
|
103
|
+
},
|
|
104
|
+
asset_issuer: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Optional asset issuer address for non-native assets"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
required: []
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "list_providers",
|
|
114
|
+
description: "List available RouteDock providers from the registry. Can filter by capability tags (returns providers matching any of the given tags) and by network.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
tags: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description: 'Comma-separated tags to filter providers (e.g., "price,stellar,dex")'
|
|
121
|
+
},
|
|
122
|
+
network: {
|
|
123
|
+
type: "string",
|
|
124
|
+
enum: ["testnet", "mainnet"],
|
|
125
|
+
description: "Filter by Stellar network"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
required: []
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// src/handlers.ts
|
|
134
|
+
import { Keypair, Horizon } from "@stellar/stellar-sdk";
|
|
135
|
+
function ok(payload) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function err(message) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: message }, null, 2) }],
|
|
143
|
+
isError: true
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function handlePayForData(args, deps2) {
|
|
147
|
+
const { url, max_amount, preferred_mode } = args;
|
|
148
|
+
const { client: client2 } = deps2;
|
|
149
|
+
const modeOptions = preferred_mode ? { forceMode: preferred_mode } : void 0;
|
|
150
|
+
const estimate = await client2.estimateCost(url, modeOptions);
|
|
151
|
+
if (estimate.amount === void 0 || isNaN(parseFloat(estimate.amount))) {
|
|
152
|
+
return err("Provider returned an undefined or invalid price");
|
|
153
|
+
}
|
|
154
|
+
if (parseFloat(estimate.amount) > parseFloat(max_amount)) {
|
|
155
|
+
return err(
|
|
156
|
+
`Provider cost ${estimate.amount} ${estimate.asset} exceeds max_amount ${max_amount} USDC`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const result = await client2.pay(url, modeOptions);
|
|
160
|
+
return ok({
|
|
161
|
+
success: true,
|
|
162
|
+
mode: result.mode,
|
|
163
|
+
amount: result.amount,
|
|
164
|
+
tx_hash: result.txHash,
|
|
165
|
+
timestamp: result.timestamp,
|
|
166
|
+
data: result.data
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function handleOpenSession(args, deps2, commitmentSecret) {
|
|
170
|
+
const { url, initial_deposit } = args;
|
|
171
|
+
const { client: client2, openSessions: openSessions2, fetchManifest } = deps2;
|
|
172
|
+
if (!commitmentSecret) {
|
|
173
|
+
return err("COMMITMENT_SECRET environment variable is required for session mode");
|
|
174
|
+
}
|
|
175
|
+
if (initial_deposit) {
|
|
176
|
+
const baseUrl = new URL(url).origin;
|
|
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();
|
|
181
|
+
const minDeposit = manifest?.pricing?.["mpp-session"]?.min_deposit ?? manifest?.pricing?.["mpp-session-ws"]?.min_deposit;
|
|
182
|
+
if (minDeposit && parseFloat(initial_deposit) < parseFloat(minDeposit)) {
|
|
183
|
+
return err(
|
|
184
|
+
`initial_deposit ${initial_deposit} is below this provider's min_deposit ${minDeposit}. RouteDock channels are pre-deployed and funded out-of-band before the agent runs \u2014 make sure the channel is funded with at least min_deposit before opening a session.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const session = await client2.openSession(url);
|
|
189
|
+
openSessions2.set(session.channelId, session);
|
|
190
|
+
return ok({
|
|
191
|
+
success: true,
|
|
192
|
+
channel_id: session.channelId,
|
|
193
|
+
open_tx_hash: session.openTxHash,
|
|
194
|
+
message: "Session opened successfully. Use stream_session to pull data and close_session to settle and release the channel collateral."
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function handleStreamSession(args, deps2) {
|
|
198
|
+
const { channel_id, max_messages } = args;
|
|
199
|
+
const { openSessions: openSessions2 } = deps2;
|
|
200
|
+
const session = openSessions2.get(channel_id);
|
|
201
|
+
if (!session) {
|
|
202
|
+
return err(
|
|
203
|
+
`No open session found for channel_id ${channel_id}. It may have already been closed, auto-closed after its wall-clock lifetime guard, or opened by a different server process.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (max_messages !== void 0 && (!Number.isInteger(max_messages) || max_messages < 1 || max_messages > MAX_STREAM_MESSAGES)) {
|
|
207
|
+
return err(
|
|
208
|
+
`max_messages must be an integer between 1 and ${MAX_STREAM_MESSAGES} (received ${max_messages})`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const limit = max_messages ?? 1;
|
|
212
|
+
const messages = [];
|
|
213
|
+
const iterator = session.stream()[Symbol.asyncIterator]();
|
|
214
|
+
try {
|
|
215
|
+
for (let i = 0; i < limit; i++) {
|
|
216
|
+
const { value, done } = await iterator.next();
|
|
217
|
+
if (done) break;
|
|
218
|
+
messages.push(value);
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: JSON.stringify(
|
|
226
|
+
{
|
|
227
|
+
success: false,
|
|
228
|
+
error: error instanceof Error ? error.message : String(error),
|
|
229
|
+
channel_id,
|
|
230
|
+
count: messages.length,
|
|
231
|
+
messages,
|
|
232
|
+
stats: session.stats()
|
|
233
|
+
},
|
|
234
|
+
null,
|
|
235
|
+
2
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
isError: true
|
|
240
|
+
};
|
|
241
|
+
} finally {
|
|
242
|
+
await iterator.return?.();
|
|
243
|
+
}
|
|
244
|
+
return ok({
|
|
245
|
+
success: true,
|
|
246
|
+
channel_id,
|
|
247
|
+
count: messages.length,
|
|
248
|
+
messages
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function handleCloseSession(args, deps2) {
|
|
252
|
+
const { channel_id } = args;
|
|
253
|
+
const { openSessions: openSessions2 } = deps2;
|
|
254
|
+
const session = openSessions2.get(channel_id);
|
|
255
|
+
if (!session) {
|
|
256
|
+
return err(
|
|
257
|
+
`No open session found for channel_id ${channel_id}. It may have already been closed or auto-closed after its wall-clock lifetime guard.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const result = await session.close();
|
|
261
|
+
openSessions2.delete(channel_id);
|
|
262
|
+
return ok({
|
|
263
|
+
success: true,
|
|
264
|
+
channel_id,
|
|
265
|
+
close_tx_hash: result.closeTxHash,
|
|
266
|
+
total_paid: result.totalPaid,
|
|
267
|
+
vouchers_issued: result.vouchersIssued
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
var HORIZON_URLS = {
|
|
271
|
+
mainnet: "https://horizon.stellar.org",
|
|
272
|
+
testnet: "https://horizon-testnet.stellar.org"
|
|
273
|
+
};
|
|
274
|
+
function normalizeBalances(balances) {
|
|
275
|
+
return balances.map((b) => ({
|
|
276
|
+
asset_type: b.asset_type,
|
|
277
|
+
asset_code: "asset_code" in b ? b.asset_code : void 0,
|
|
278
|
+
asset_issuer: "asset_issuer" in b ? b.asset_issuer : void 0,
|
|
279
|
+
balance: b.balance
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
async function handleCheckBalance(args, deps2) {
|
|
283
|
+
const { asset_code, asset_issuer } = args;
|
|
284
|
+
const { stellarSecret, stellarNetwork, createHorizonServer } = deps2;
|
|
285
|
+
const keypair = Keypair.fromSecret(stellarSecret);
|
|
286
|
+
const horizonUrl = HORIZON_URLS[stellarNetwork];
|
|
287
|
+
const horizonServer = createHorizonServer ? createHorizonServer(horizonUrl) : new Horizon.Server(horizonUrl);
|
|
288
|
+
const account = await horizonServer.loadAccount(keypair.publicKey());
|
|
289
|
+
const balances = normalizeBalances(account.balances);
|
|
290
|
+
if (asset_code && asset_issuer) {
|
|
291
|
+
const balance = balances.find(
|
|
292
|
+
(b) => b.asset_code === asset_code && b.asset_issuer === asset_issuer
|
|
293
|
+
);
|
|
294
|
+
return ok({
|
|
295
|
+
asset: asset_code,
|
|
296
|
+
issuer: asset_issuer,
|
|
297
|
+
balance: balance ? balance.balance : "0",
|
|
298
|
+
account: keypair.publicKey()
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (asset_code) {
|
|
302
|
+
const balance = balances.find((b) => b.asset_code === asset_code);
|
|
303
|
+
return ok({
|
|
304
|
+
asset: asset_code,
|
|
305
|
+
balance: balance ? balance.balance : "0",
|
|
306
|
+
account: keypair.publicKey()
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
const nativeBalance = balances.find((b) => b.asset_type === "native");
|
|
310
|
+
return ok({
|
|
311
|
+
asset: "XLM",
|
|
312
|
+
balance: nativeBalance ? nativeBalance.balance : "0",
|
|
313
|
+
account: keypair.publicKey()
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async function handleListProviders(args, deps2) {
|
|
317
|
+
const { tags, network } = args;
|
|
318
|
+
const { supabase: supabase2 } = deps2;
|
|
319
|
+
if (!supabase2) {
|
|
320
|
+
return err(
|
|
321
|
+
"SUPABASE_URL and SUPABASE_KEY environment variables are required for provider registry access"
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
let query = supabase2.from("providers").select("*");
|
|
325
|
+
if (network && query.eq) {
|
|
326
|
+
query = query.eq("network", network);
|
|
327
|
+
}
|
|
328
|
+
if (tags) {
|
|
329
|
+
const tagList = tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
330
|
+
if (tagList.length > 0 && query.overlaps) {
|
|
331
|
+
query = query.overlaps("tags", tagList);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const { data, error } = await query;
|
|
335
|
+
if (error) {
|
|
336
|
+
return err(`Failed to fetch providers: ${error.message}`);
|
|
337
|
+
}
|
|
338
|
+
const rows = data ?? [];
|
|
339
|
+
return ok({
|
|
340
|
+
success: true,
|
|
341
|
+
count: rows.length,
|
|
342
|
+
providers: rows.map((p) => ({
|
|
343
|
+
name: p.name,
|
|
344
|
+
description: p.description,
|
|
345
|
+
network: p.network,
|
|
346
|
+
asset: p.asset,
|
|
347
|
+
modes: p.modes,
|
|
348
|
+
tags: p.tags,
|
|
349
|
+
base_url: p.base_url
|
|
350
|
+
}))
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/index.ts
|
|
355
|
+
var envPath = process.env.ROUTEDOCK_ENV_FILE;
|
|
356
|
+
if (envPath) {
|
|
357
|
+
loadDotenv({ path: envPath });
|
|
358
|
+
} else {
|
|
359
|
+
loadDotenv();
|
|
360
|
+
}
|
|
361
|
+
var STELLAR_SECRET = process.env.STELLAR_SECRET || process.env.ROUTEDOCK_WALLET_SECRET;
|
|
362
|
+
var STELLAR_NETWORK = process.env.STELLAR_NETWORK || "testnet";
|
|
363
|
+
var COMMITMENT_SECRET = process.env.COMMITMENT_SECRET;
|
|
364
|
+
var SUPABASE_URL = process.env.SUPABASE_URL;
|
|
365
|
+
var SUPABASE_KEY = process.env.SUPABASE_KEY || process.env.SUPABASE_SERVICE_KEY;
|
|
366
|
+
if (process.env.SUPABASE_SERVICE_KEY) {
|
|
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");
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
var ROUTEDOCK_DAILY_CAP = process.env.ROUTEDOCK_DAILY_CAP;
|
|
374
|
+
if (!ROUTEDOCK_DAILY_CAP) {
|
|
375
|
+
console.error("Error: ROUTEDOCK_DAILY_CAP environment variable is required to prevent unbounded spending");
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
var spendStorePath = process.env.ROUTEDOCK_SPEND_STORE_PATH || path.join(os.homedir(), ".routedock", "spend.json");
|
|
379
|
+
var client = new RouteDockClient({
|
|
380
|
+
wallet: STELLAR_SECRET,
|
|
381
|
+
network: STELLAR_NETWORK,
|
|
382
|
+
commitmentSecret: COMMITMENT_SECRET,
|
|
383
|
+
spendCap: { daily: ROUTEDOCK_DAILY_CAP, asset: "USDC" },
|
|
384
|
+
spendStore: new FileSpendStore(spendStorePath)
|
|
385
|
+
});
|
|
386
|
+
var supabase = null;
|
|
387
|
+
if (SUPABASE_URL && SUPABASE_KEY) {
|
|
388
|
+
supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
|
389
|
+
}
|
|
390
|
+
var openSessions = /* @__PURE__ */ new Map();
|
|
391
|
+
var deps = {
|
|
392
|
+
client,
|
|
393
|
+
openSessions,
|
|
394
|
+
supabase,
|
|
395
|
+
stellarSecret: STELLAR_SECRET,
|
|
396
|
+
stellarNetwork: STELLAR_NETWORK
|
|
397
|
+
};
|
|
398
|
+
var server = new Server(
|
|
399
|
+
{
|
|
400
|
+
name: "@routedock/mcp-server",
|
|
401
|
+
version: "0.1.0"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
capabilities: {
|
|
405
|
+
tools: {}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
410
|
+
return { tools: TOOLS };
|
|
411
|
+
});
|
|
412
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
413
|
+
const { name, arguments: args } = request.params;
|
|
414
|
+
try {
|
|
415
|
+
switch (name) {
|
|
416
|
+
case "pay_for_data":
|
|
417
|
+
return await handlePayForData(args, deps);
|
|
418
|
+
case "open_session":
|
|
419
|
+
return await handleOpenSession(args, deps, COMMITMENT_SECRET);
|
|
420
|
+
case "stream_session":
|
|
421
|
+
return await handleStreamSession(args, deps);
|
|
422
|
+
case "close_session":
|
|
423
|
+
return await handleCloseSession(args, deps);
|
|
424
|
+
case "check_balance":
|
|
425
|
+
return await handleCheckBalance(args, deps);
|
|
426
|
+
case "list_providers":
|
|
427
|
+
return await handleListProviders(args, deps);
|
|
428
|
+
default:
|
|
429
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return {
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: "text",
|
|
436
|
+
text: JSON.stringify({
|
|
437
|
+
success: false,
|
|
438
|
+
error: error instanceof Error ? error.message : String(error)
|
|
439
|
+
}, null, 2)
|
|
440
|
+
}
|
|
441
|
+
],
|
|
442
|
+
isError: true
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
async function main() {
|
|
447
|
+
const transport = new StdioServerTransport();
|
|
448
|
+
await server.connect(transport);
|
|
449
|
+
console.error("@routedock/mcp-server running on stdio");
|
|
450
|
+
}
|
|
451
|
+
main().catch((error) => {
|
|
452
|
+
console.error("Server error:", error);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@routedock/mcp-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for RouteDock Stellar payments - exposes payment tools to LLM agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/winsznx/routedock.git",
|
|
9
|
+
"directory": "packages/mcp-server"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"routedock-mcp-server": "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
23
|
+
"@stellar/stellar-sdk": "^14.6.1",
|
|
24
|
+
"@supabase/supabase-js": "^2.47.0",
|
|
25
|
+
"dotenv": "^17.4.2",
|
|
26
|
+
"@routedock/routedock": "^0.2.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"tsup": "^8.0.0",
|
|
31
|
+
"tsx": "^4.22.3",
|
|
32
|
+
"typescript": "^5.4.5"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "tsx --experimental-test-module-mocks --test \"src/**/*.test.ts\"",
|
|
39
|
+
"start": "node dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
}
|