@openclawcash/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/README.md +272 -0
- package/openclawcash-mcp.mjs +886 -0
- package/package.json +45 -0
package/.env.example
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# OpenClawCash MCP Server
|
|
2
|
+
|
|
3
|
+
This folder contains the public OpenClawCash stdio MCP server package that exposes OpenClawCash agent wallet operations as MCP tools.
|
|
4
|
+
|
|
5
|
+
It is a thin adapter over the existing OpenClawCash agent API at `https://openclawcash.com/api/agent/*`.
|
|
6
|
+
|
|
7
|
+
Canonical docs page: `https://openclawcash.com/mcp`
|
|
8
|
+
|
|
9
|
+
## What It Exposes
|
|
10
|
+
|
|
11
|
+
- `wallets_list`
|
|
12
|
+
- `wallet_get`
|
|
13
|
+
- `transactions_list`
|
|
14
|
+
- `balances_get`
|
|
15
|
+
- `supported_tokens_list`
|
|
16
|
+
- `transfer_send`
|
|
17
|
+
- `swap_quote`
|
|
18
|
+
- `swap_execute`
|
|
19
|
+
- `approve_token`
|
|
20
|
+
- `wallet_create`
|
|
21
|
+
- `wallet_import`
|
|
22
|
+
|
|
23
|
+
It also exposes two lightweight MCP resources:
|
|
24
|
+
|
|
25
|
+
- `openclawcash://approval-modes`
|
|
26
|
+
- `openclawcash://quickstart`
|
|
27
|
+
|
|
28
|
+
## Run It
|
|
29
|
+
|
|
30
|
+
Direct from npm:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx -y @openclawcash/mcp-server
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
From this folder during local development:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node openclawcash-mcp.mjs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If this package is installed globally or linked as a CLI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
openclawcash-mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Quick self-test:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx -y @openclawcash/mcp-server --self-test
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
Set one of these environment variables:
|
|
57
|
+
|
|
58
|
+
- `OPENCLAWCASH_AGENT_KEY`
|
|
59
|
+
- `AGENTWALLETAPI_KEY`
|
|
60
|
+
|
|
61
|
+
Optional base URL override:
|
|
62
|
+
|
|
63
|
+
- `OPENCLAWCASH_BASE_URL`
|
|
64
|
+
- `AGENTWALLETAPI_URL`
|
|
65
|
+
|
|
66
|
+
The server also looks for env files in this order:
|
|
67
|
+
|
|
68
|
+
1. package `.env`
|
|
69
|
+
2. current working directory `.env.local`
|
|
70
|
+
3. current working directory `.env`
|
|
71
|
+
|
|
72
|
+
Example package `.env`:
|
|
73
|
+
|
|
74
|
+
```env
|
|
75
|
+
OPENCLAWCASH_AGENT_KEY=occ_your_api_key_here
|
|
76
|
+
OPENCLAWCASH_BASE_URL=https://openclawcash.com
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## OpenClaw Example
|
|
80
|
+
|
|
81
|
+
You can print a config snippet with:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx -y @openclawcash/mcp-server --print-openclaw-config
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Typical shape:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"openclawcash": {
|
|
93
|
+
"command": "npx",
|
|
94
|
+
"args": ["-y", "@openclawcash/mcp-server"],
|
|
95
|
+
"env": {
|
|
96
|
+
"OPENCLAWCASH_AGENT_KEY": "occ_your_api_key_here",
|
|
97
|
+
"OPENCLAWCASH_BASE_URL": "https://openclawcash.com"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If OpenClaw expects the same MCP structure in a different config location, keep the same `command`, `args`, and `env` values and adapt only the outer wrapper required by that client.
|
|
105
|
+
|
|
106
|
+
## Claude Desktop Example
|
|
107
|
+
|
|
108
|
+
You can print a config snippet with:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx -y @openclawcash/mcp-server --print-claude-config
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Typical config shape:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"openclawcash": {
|
|
120
|
+
"command": "npx",
|
|
121
|
+
"args": ["-y", "@openclawcash/mcp-server"],
|
|
122
|
+
"env": {
|
|
123
|
+
"OPENCLAWCASH_AGENT_KEY": "occ_your_api_key_here",
|
|
124
|
+
"OPENCLAWCASH_BASE_URL": "https://openclawcash.com"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Cursor Example
|
|
132
|
+
|
|
133
|
+
You can print a config snippet with:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx -y @openclawcash/mcp-server --print-cursor-config
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If your Cursor build supports MCP servers, the config shape is the same idea: run the server as a local stdio command and pass the API key in env.
|
|
140
|
+
|
|
141
|
+
Typical shape:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"openclawcash": {
|
|
147
|
+
"command": "npx",
|
|
148
|
+
"args": ["-y", "@openclawcash/mcp-server"],
|
|
149
|
+
"env": {
|
|
150
|
+
"OPENCLAWCASH_AGENT_KEY": "occ_your_api_key_here",
|
|
151
|
+
"OPENCLAWCASH_BASE_URL": "https://openclawcash.com"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
If your Cursor build expects the config in a different file location or format, keep the same command, args, and env values and adapt only the wrapper structure.
|
|
159
|
+
|
|
160
|
+
## VS Code Example
|
|
161
|
+
|
|
162
|
+
You can print a config snippet with:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx -y @openclawcash/mcp-server --print-vscode-config
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If your VS Code AI extension or MCP-compatible tooling supports local stdio MCP servers, use the same command pattern:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"mcpServers": {
|
|
173
|
+
"openclawcash": {
|
|
174
|
+
"command": "npx",
|
|
175
|
+
"args": ["-y", "@openclawcash/mcp-server"],
|
|
176
|
+
"env": {
|
|
177
|
+
"OPENCLAWCASH_AGENT_KEY": "occ_your_api_key_here",
|
|
178
|
+
"OPENCLAWCASH_BASE_URL": "https://openclawcash.com"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If your VS Code setup uses a different config file location or wrapper format, keep the same `command`, `args`, and `env` values and adapt only the surrounding JSON structure required by that extension.
|
|
186
|
+
|
|
187
|
+
## Verify Before IDE Setup
|
|
188
|
+
|
|
189
|
+
Use these commands before wiring the server into any IDE:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx -y @openclawcash/mcp-server --help
|
|
193
|
+
npx -y @openclawcash/mcp-server --self-test
|
|
194
|
+
npx -y @openclawcash/mcp-server --print-openclaw-config
|
|
195
|
+
npx -y @openclawcash/mcp-server --print-claude-config
|
|
196
|
+
npx -y @openclawcash/mcp-server --print-cursor-config
|
|
197
|
+
npx -y @openclawcash/mcp-server --print-vscode-config
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
What `--self-test` checks:
|
|
201
|
+
|
|
202
|
+
- the server file runs
|
|
203
|
+
- the MCP metadata is valid enough for initialization
|
|
204
|
+
- the resolved base URL is correct
|
|
205
|
+
- whether an agent key is present in env or env files
|
|
206
|
+
|
|
207
|
+
## Approval Model
|
|
208
|
+
|
|
209
|
+
Write tools are high-risk. The intended agent behavior is:
|
|
210
|
+
|
|
211
|
+
- Ask once at the start of the first write-intent:
|
|
212
|
+
- `confirm_each_write`
|
|
213
|
+
- `operate_on_my_behalf`
|
|
214
|
+
- If the user chooses `operate_on_my_behalf`, the agent should stop re-asking for each later transfer in the same session and only ask for missing execution details.
|
|
215
|
+
|
|
216
|
+
The MCP server itself does not hold approval memory. The MCP client or agent runtime should remember the selected mode for the session.
|
|
217
|
+
|
|
218
|
+
## Notes
|
|
219
|
+
|
|
220
|
+
- `swap_quote` is read-only.
|
|
221
|
+
- `transfer_send`, `swap_execute`, `approve_token`, `wallet_create`, and `wallet_import` are write tools.
|
|
222
|
+
- The server uses stdio transport and is intended for MCP-compatible desktop or agent clients.
|
|
223
|
+
|
|
224
|
+
## Standalone Packaging Notes
|
|
225
|
+
|
|
226
|
+
- This folder has its own [package.json](/home/slicks/projects/OpenClawCash/AIWalleControll/mcp/package.json).
|
|
227
|
+
- The CLI name is `openclawcash-mcp`.
|
|
228
|
+
- The runtime no longer depends on repo-root paths.
|
|
229
|
+
- It can be distributed independently from the main web app repo.
|
|
230
|
+
- The intended public install path is `npx -y @openclawcash/mcp-server`.
|
|
231
|
+
|
|
232
|
+
## Release Checklist
|
|
233
|
+
|
|
234
|
+
Before publishing a new MCP package release:
|
|
235
|
+
|
|
236
|
+
1. Update the version in [package.json](/home/slicks/projects/OpenClawCash/AIWalleControll/mcp/package.json).
|
|
237
|
+
2. Run the package self-test:
|
|
238
|
+
```bash
|
|
239
|
+
cd mcp
|
|
240
|
+
node openclawcash-mcp.mjs --self-test
|
|
241
|
+
```
|
|
242
|
+
3. Verify the config helpers:
|
|
243
|
+
```bash
|
|
244
|
+
cd mcp
|
|
245
|
+
node openclawcash-mcp.mjs --print-openclaw-config
|
|
246
|
+
node openclawcash-mcp.mjs --print-claude-config
|
|
247
|
+
node openclawcash-mcp.mjs --print-cursor-config
|
|
248
|
+
node openclawcash-mcp.mjs --print-vscode-config
|
|
249
|
+
```
|
|
250
|
+
4. Confirm the package contents:
|
|
251
|
+
```bash
|
|
252
|
+
cd mcp
|
|
253
|
+
npm pack --dry-run
|
|
254
|
+
```
|
|
255
|
+
5. Review that no secrets are present:
|
|
256
|
+
- no `.env`
|
|
257
|
+
- no local credentials
|
|
258
|
+
- no private test data
|
|
259
|
+
6. Publish from the package folder:
|
|
260
|
+
```bash
|
|
261
|
+
cd mcp
|
|
262
|
+
npm publish --access public
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
If you want to test the package locally before publishing:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
cd mcp
|
|
269
|
+
npm pack
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Then install the generated tarball in a separate test directory and verify the CLI runs there without repo context.
|
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
const SERVER_NAME = "openclawcash";
|
|
10
|
+
const SERVER_VERSION = "0.1.0";
|
|
11
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
12
|
+
const DEFAULT_BASE_URL = "https://openclawcash.com";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const packageRoot = __dirname;
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const PACKAGE_NAME = "@openclawcash/mcp-server";
|
|
19
|
+
|
|
20
|
+
const envFileCandidates = [
|
|
21
|
+
path.join(packageRoot, ".env"),
|
|
22
|
+
path.join(cwd, ".env.local"),
|
|
23
|
+
path.join(cwd, ".env"),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function parseEnvFile(filePath) {
|
|
27
|
+
if (!fs.existsSync(filePath)) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
32
|
+
const entries = {};
|
|
33
|
+
|
|
34
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
35
|
+
const line = rawLine.trim();
|
|
36
|
+
if (!line || line.startsWith("#")) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const separatorIndex = line.indexOf("=");
|
|
41
|
+
if (separatorIndex === -1) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
46
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
47
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
48
|
+
value = value.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (key && !(key in entries)) {
|
|
52
|
+
entries[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fileEnv = envFileCandidates.reduce((acc, candidate) => ({ ...acc, ...parseEnvFile(candidate) }), {});
|
|
60
|
+
|
|
61
|
+
function getEnv(name, fallbackNames = []) {
|
|
62
|
+
const keys = [name, ...fallbackNames];
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const value = process.env[key] ?? fileEnv[key];
|
|
65
|
+
if (value && String(value).trim()) {
|
|
66
|
+
return String(value).trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toTextResult(data, isError = false) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: typeof data === "string" ? data : JSON.stringify(data, null, 2),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
...(isError ? { isError: true } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function selectorDescription() {
|
|
85
|
+
return "Provide exactly one of walletId, walletLabel, or walletAddress.";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const walletSelectorBaseSchema = z.object({
|
|
89
|
+
walletId: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
|
|
90
|
+
walletLabel: z.string().min(1).optional(),
|
|
91
|
+
walletAddress: z.string().min(1).optional(),
|
|
92
|
+
chain: z.enum(["evm", "solana"]).optional(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const walletSelectorSchema = walletSelectorBaseSchema.refine(
|
|
96
|
+
(data) => [data.walletId, data.walletLabel, data.walletAddress].filter((value) => value !== undefined).length === 1,
|
|
97
|
+
{
|
|
98
|
+
message: selectorDescription(),
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const walletsListArgsSchema = z.object({
|
|
103
|
+
includeBalances: z.boolean().optional(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const balancesArgsSchema = z
|
|
107
|
+
.object({
|
|
108
|
+
walletId: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
|
|
109
|
+
walletAddress: z.string().min(1).optional(),
|
|
110
|
+
chain: z.enum(["evm", "solana"]).optional(),
|
|
111
|
+
token: z.string().min(1).optional(),
|
|
112
|
+
tokenAddress: z.string().min(1).optional(),
|
|
113
|
+
})
|
|
114
|
+
.refine((data) => [data.walletId, data.walletAddress].filter((value) => value !== undefined).length === 1, {
|
|
115
|
+
message: "Provide exactly one of walletId or walletAddress.",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const transferArgsSchema = walletSelectorBaseSchema
|
|
119
|
+
.extend({
|
|
120
|
+
to: z.string().min(1),
|
|
121
|
+
amount: z.string().min(1).optional(),
|
|
122
|
+
value: z.string().min(1).optional(),
|
|
123
|
+
token: z.string().min(1).optional(),
|
|
124
|
+
memo: z.string().min(1).optional(),
|
|
125
|
+
})
|
|
126
|
+
.refine((data) => [data.walletId, data.walletLabel, data.walletAddress].filter((value) => value !== undefined).length === 1, {
|
|
127
|
+
message: selectorDescription(),
|
|
128
|
+
})
|
|
129
|
+
.refine((data) => [data.amount, data.value].filter((value) => value !== undefined).length === 1, {
|
|
130
|
+
message: "Provide exactly one of amount or value.",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const swapQuoteArgsSchema = z.object({
|
|
134
|
+
network: z.string().min(1),
|
|
135
|
+
tokenIn: z.string().min(1),
|
|
136
|
+
tokenOut: z.string().min(1),
|
|
137
|
+
amountIn: z.string().min(1),
|
|
138
|
+
chain: z.enum(["evm", "solana"]).optional(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const swapExecuteArgsSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
walletId: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
|
|
144
|
+
walletAddress: z.string().min(1).optional(),
|
|
145
|
+
tokenIn: z.string().min(1),
|
|
146
|
+
tokenOut: z.string().min(1),
|
|
147
|
+
amountIn: z.string().min(1),
|
|
148
|
+
slippage: z.number().positive().optional(),
|
|
149
|
+
chain: z.enum(["evm", "solana"]).optional(),
|
|
150
|
+
})
|
|
151
|
+
.refine((data) => [data.walletId, data.walletAddress].filter((value) => value !== undefined).length === 1, {
|
|
152
|
+
message: "Provide exactly one of walletId or walletAddress.",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const approveArgsSchema = z
|
|
156
|
+
.object({
|
|
157
|
+
walletId: z.union([z.number().int().positive(), z.string().min(1)]).optional(),
|
|
158
|
+
walletAddress: z.string().min(1).optional(),
|
|
159
|
+
tokenAddress: z.string().min(1),
|
|
160
|
+
spender: z.string().min(1),
|
|
161
|
+
amount: z.string().min(1),
|
|
162
|
+
chain: z.enum(["evm", "solana"]).optional(),
|
|
163
|
+
})
|
|
164
|
+
.refine((data) => [data.walletId, data.walletAddress].filter((value) => value !== undefined).length === 1, {
|
|
165
|
+
message: "Provide exactly one of walletId or walletAddress.",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const supportedTokensArgsSchema = z.object({
|
|
169
|
+
network: z.string().min(1).optional(),
|
|
170
|
+
chain: z.enum(["evm", "solana"]).optional(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const createWalletArgsSchema = z.object({
|
|
174
|
+
label: z.string().min(1),
|
|
175
|
+
network: z.enum(["sepolia", "mainnet", "solana-devnet", "solana-testnet", "solana-mainnet"]).optional(),
|
|
176
|
+
exportPassphrase: z.string().trim().min(12),
|
|
177
|
+
confirmExportPassphraseSaved: z.literal(true),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const importWalletArgsSchema = z.object({
|
|
181
|
+
label: z.string().min(1),
|
|
182
|
+
network: z.enum(["mainnet", "solana-mainnet"]),
|
|
183
|
+
privateKey: z.string().min(1),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
function queryString(params) {
|
|
187
|
+
const search = new URLSearchParams();
|
|
188
|
+
for (const [key, value] of Object.entries(params)) {
|
|
189
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
190
|
+
search.set(key, String(value));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const result = search.toString();
|
|
194
|
+
return result ? `?${result}` : "";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getBaseUrl() {
|
|
198
|
+
return (getEnv("OPENCLAWCASH_BASE_URL", ["AGENTWALLETAPI_URL"]) || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getAgentKey() {
|
|
202
|
+
return getEnv("OPENCLAWCASH_AGENT_KEY", ["AGENTWALLETAPI_KEY"]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function requireAgentKey() {
|
|
206
|
+
const agentKey = getAgentKey();
|
|
207
|
+
if (!agentKey) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Missing OpenClawCash agent key. Set OPENCLAWCASH_AGENT_KEY or AGENTWALLETAPI_KEY in the environment, package .env, .env.local, or .env.",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return agentKey;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function callAgentApi({ method, pathName, query, body, requireAuth = true }) {
|
|
216
|
+
const headers = {
|
|
217
|
+
Accept: "application/json",
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (body !== undefined) {
|
|
221
|
+
headers["Content-Type"] = "application/json";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (requireAuth) {
|
|
225
|
+
headers["X-Agent-Key"] = requireAgentKey();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const url = `${getBaseUrl()}${pathName}${queryString(query || {})}`;
|
|
229
|
+
const response = await fetch(url, {
|
|
230
|
+
method,
|
|
231
|
+
headers,
|
|
232
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const rawText = await response.text();
|
|
236
|
+
let payload;
|
|
237
|
+
try {
|
|
238
|
+
payload = rawText ? JSON.parse(rawText) : null;
|
|
239
|
+
} catch {
|
|
240
|
+
payload = rawText;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
const message =
|
|
245
|
+
typeof payload === "object" && payload && "message" in payload
|
|
246
|
+
? payload.message
|
|
247
|
+
: typeof payload === "string" && payload
|
|
248
|
+
? payload
|
|
249
|
+
: `OpenClawCash API request failed with status ${response.status}`;
|
|
250
|
+
|
|
251
|
+
const error = new Error(message);
|
|
252
|
+
error.status = response.status;
|
|
253
|
+
error.payload = payload;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return payload;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function withWriteSafety(description) {
|
|
261
|
+
return `${description} High-risk write tool: callers should establish session approval mode before using it.`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const tools = [
|
|
265
|
+
{
|
|
266
|
+
name: "wallets_list",
|
|
267
|
+
description: "List managed wallets accessible to the configured OpenClawCash agent key.",
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: "object",
|
|
270
|
+
properties: {
|
|
271
|
+
includeBalances: { type: "boolean", description: "Include native balance previews in the wallet list." },
|
|
272
|
+
},
|
|
273
|
+
additionalProperties: false,
|
|
274
|
+
},
|
|
275
|
+
parse: (args) => walletsListArgsSchema.parse(args ?? {}),
|
|
276
|
+
execute: async (args) =>
|
|
277
|
+
callAgentApi({
|
|
278
|
+
method: "GET",
|
|
279
|
+
pathName: "/api/agent/wallets",
|
|
280
|
+
query: { includeBalances: args.includeBalances ? "true" : undefined },
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "wallet_get",
|
|
285
|
+
description: "Get one managed wallet with native and token balances.",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
walletId: { oneOf: [{ type: "integer" }, { type: "string" }], description: selectorDescription() },
|
|
290
|
+
walletLabel: { type: "string", description: selectorDescription() },
|
|
291
|
+
walletAddress: { type: "string", description: selectorDescription() },
|
|
292
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
293
|
+
},
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
},
|
|
296
|
+
parse: (args) => walletSelectorSchema.parse(args ?? {}),
|
|
297
|
+
execute: async (args) =>
|
|
298
|
+
callAgentApi({
|
|
299
|
+
method: "GET",
|
|
300
|
+
pathName: "/api/agent/wallet",
|
|
301
|
+
query: args,
|
|
302
|
+
}),
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "transactions_list",
|
|
306
|
+
description: "List merged transaction history for one managed wallet.",
|
|
307
|
+
inputSchema: {
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: {
|
|
310
|
+
walletId: { oneOf: [{ type: "integer" }, { type: "string" }], description: selectorDescription() },
|
|
311
|
+
walletLabel: { type: "string", description: selectorDescription() },
|
|
312
|
+
walletAddress: { type: "string", description: selectorDescription() },
|
|
313
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
314
|
+
},
|
|
315
|
+
additionalProperties: false,
|
|
316
|
+
},
|
|
317
|
+
parse: (args) => walletSelectorSchema.parse(args ?? {}),
|
|
318
|
+
execute: async (args) =>
|
|
319
|
+
callAgentApi({
|
|
320
|
+
method: "GET",
|
|
321
|
+
pathName: "/api/agent/transactions",
|
|
322
|
+
query: args,
|
|
323
|
+
}),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "balances_get",
|
|
327
|
+
description: "Get native and token balances for one managed wallet.",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
type: "object",
|
|
330
|
+
properties: {
|
|
331
|
+
walletId: { oneOf: [{ type: "integer" }, { type: "string" }] },
|
|
332
|
+
walletAddress: { type: "string" },
|
|
333
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
334
|
+
token: { type: "string" },
|
|
335
|
+
tokenAddress: { type: "string" },
|
|
336
|
+
},
|
|
337
|
+
additionalProperties: false,
|
|
338
|
+
},
|
|
339
|
+
parse: (args) => balancesArgsSchema.parse(args ?? {}),
|
|
340
|
+
execute: async (args) =>
|
|
341
|
+
callAgentApi({
|
|
342
|
+
method: "POST",
|
|
343
|
+
pathName: "/api/agent/token-balance",
|
|
344
|
+
body: args,
|
|
345
|
+
}),
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "supported_tokens_list",
|
|
349
|
+
description: "List recommended supported tokens and guidance for a network or chain.",
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {
|
|
353
|
+
network: { type: "string", description: "Example: mainnet, sepolia, solana-mainnet." },
|
|
354
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
355
|
+
},
|
|
356
|
+
additionalProperties: false,
|
|
357
|
+
},
|
|
358
|
+
parse: (args) => supportedTokensArgsSchema.parse(args ?? {}),
|
|
359
|
+
execute: async (args) =>
|
|
360
|
+
callAgentApi({
|
|
361
|
+
method: "GET",
|
|
362
|
+
pathName: "/api/agent/supported-tokens",
|
|
363
|
+
query: args,
|
|
364
|
+
requireAuth: false,
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "transfer_send",
|
|
369
|
+
description: withWriteSafety("Send a native asset or token transfer from a managed wallet."),
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
walletId: { oneOf: [{ type: "integer" }, { type: "string" }] },
|
|
374
|
+
walletLabel: { type: "string" },
|
|
375
|
+
walletAddress: { type: "string" },
|
|
376
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
377
|
+
to: { type: "string" },
|
|
378
|
+
amount: { type: "string", description: "Human-readable amount." },
|
|
379
|
+
value: { type: "string", description: "Base-unit amount." },
|
|
380
|
+
token: { type: "string" },
|
|
381
|
+
memo: { type: "string", description: "Solana-only memo." },
|
|
382
|
+
},
|
|
383
|
+
required: ["to"],
|
|
384
|
+
additionalProperties: false,
|
|
385
|
+
},
|
|
386
|
+
parse: (args) => transferArgsSchema.parse(args ?? {}),
|
|
387
|
+
execute: async (args) =>
|
|
388
|
+
callAgentApi({
|
|
389
|
+
method: "POST",
|
|
390
|
+
pathName: "/api/agent/transfer",
|
|
391
|
+
body: args,
|
|
392
|
+
}),
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "swap_quote",
|
|
396
|
+
description: "Get a quote for an OpenClawCash swap before execution.",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
network: { type: "string" },
|
|
401
|
+
tokenIn: { type: "string" },
|
|
402
|
+
tokenOut: { type: "string" },
|
|
403
|
+
amountIn: { type: "string" },
|
|
404
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
405
|
+
},
|
|
406
|
+
required: ["network", "tokenIn", "tokenOut", "amountIn"],
|
|
407
|
+
additionalProperties: false,
|
|
408
|
+
},
|
|
409
|
+
parse: (args) => swapQuoteArgsSchema.parse(args ?? {}),
|
|
410
|
+
execute: async (args) => {
|
|
411
|
+
const { network, ...body } = args;
|
|
412
|
+
return callAgentApi({
|
|
413
|
+
method: "POST",
|
|
414
|
+
pathName: "/api/agent/quote",
|
|
415
|
+
query: { network },
|
|
416
|
+
body,
|
|
417
|
+
requireAuth: false,
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "swap_execute",
|
|
423
|
+
description: withWriteSafety("Execute a swap through OpenClawCash."),
|
|
424
|
+
inputSchema: {
|
|
425
|
+
type: "object",
|
|
426
|
+
properties: {
|
|
427
|
+
walletId: { oneOf: [{ type: "integer" }, { type: "string" }] },
|
|
428
|
+
walletAddress: { type: "string" },
|
|
429
|
+
tokenIn: { type: "string" },
|
|
430
|
+
tokenOut: { type: "string" },
|
|
431
|
+
amountIn: { type: "string" },
|
|
432
|
+
slippage: { type: "number" },
|
|
433
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
434
|
+
},
|
|
435
|
+
required: ["tokenIn", "tokenOut", "amountIn"],
|
|
436
|
+
additionalProperties: false,
|
|
437
|
+
},
|
|
438
|
+
parse: (args) => swapExecuteArgsSchema.parse(args ?? {}),
|
|
439
|
+
execute: async (args) =>
|
|
440
|
+
callAgentApi({
|
|
441
|
+
method: "POST",
|
|
442
|
+
pathName: "/api/agent/swap",
|
|
443
|
+
body: args,
|
|
444
|
+
}),
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "approve_token",
|
|
448
|
+
description: withWriteSafety("Approve ERC-20 token spending for a managed wallet."),
|
|
449
|
+
inputSchema: {
|
|
450
|
+
type: "object",
|
|
451
|
+
properties: {
|
|
452
|
+
walletId: { oneOf: [{ type: "integer" }, { type: "string" }] },
|
|
453
|
+
walletAddress: { type: "string" },
|
|
454
|
+
tokenAddress: { type: "string" },
|
|
455
|
+
spender: { type: "string" },
|
|
456
|
+
amount: { type: "string" },
|
|
457
|
+
chain: { type: "string", enum: ["evm", "solana"] },
|
|
458
|
+
},
|
|
459
|
+
required: ["tokenAddress", "spender", "amount"],
|
|
460
|
+
additionalProperties: false,
|
|
461
|
+
},
|
|
462
|
+
parse: (args) => approveArgsSchema.parse(args ?? {}),
|
|
463
|
+
execute: async (args) =>
|
|
464
|
+
callAgentApi({
|
|
465
|
+
method: "POST",
|
|
466
|
+
pathName: "/api/agent/approve",
|
|
467
|
+
body: args,
|
|
468
|
+
}),
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: "wallet_create",
|
|
472
|
+
description: withWriteSafety("Create a new managed wallet under the configured agent key."),
|
|
473
|
+
inputSchema: {
|
|
474
|
+
type: "object",
|
|
475
|
+
properties: {
|
|
476
|
+
label: { type: "string" },
|
|
477
|
+
network: {
|
|
478
|
+
type: "string",
|
|
479
|
+
enum: ["sepolia", "mainnet", "solana-devnet", "solana-testnet", "solana-mainnet"],
|
|
480
|
+
},
|
|
481
|
+
exportPassphrase: { type: "string", minLength: 12 },
|
|
482
|
+
confirmExportPassphraseSaved: { type: "boolean", const: true },
|
|
483
|
+
},
|
|
484
|
+
required: ["label", "exportPassphrase", "confirmExportPassphraseSaved"],
|
|
485
|
+
additionalProperties: false,
|
|
486
|
+
},
|
|
487
|
+
parse: (args) => createWalletArgsSchema.parse(args ?? {}),
|
|
488
|
+
execute: async (args) =>
|
|
489
|
+
callAgentApi({
|
|
490
|
+
method: "POST",
|
|
491
|
+
pathName: "/api/agent/wallets/create",
|
|
492
|
+
body: args,
|
|
493
|
+
}),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "wallet_import",
|
|
497
|
+
description: withWriteSafety("Import a mainnet or Solana mainnet wallet under the configured agent key."),
|
|
498
|
+
inputSchema: {
|
|
499
|
+
type: "object",
|
|
500
|
+
properties: {
|
|
501
|
+
label: { type: "string" },
|
|
502
|
+
network: { type: "string", enum: ["mainnet", "solana-mainnet"] },
|
|
503
|
+
privateKey: { type: "string" },
|
|
504
|
+
},
|
|
505
|
+
required: ["label", "network", "privateKey"],
|
|
506
|
+
additionalProperties: false,
|
|
507
|
+
},
|
|
508
|
+
parse: (args) => importWalletArgsSchema.parse(args ?? {}),
|
|
509
|
+
execute: async (args) =>
|
|
510
|
+
callAgentApi({
|
|
511
|
+
method: "POST",
|
|
512
|
+
pathName: "/api/agent/wallets/import",
|
|
513
|
+
body: args,
|
|
514
|
+
}),
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
const resources = [
|
|
519
|
+
{
|
|
520
|
+
uri: "openclawcash://approval-modes",
|
|
521
|
+
name: "OpenClawCash Approval Modes",
|
|
522
|
+
mimeType: "text/markdown",
|
|
523
|
+
text: [
|
|
524
|
+
"# OpenClawCash Approval Modes",
|
|
525
|
+
"",
|
|
526
|
+
"- `confirm_each_write`: ask before every transfer, swap, approval, import, or wallet creation.",
|
|
527
|
+
"- `operate_on_my_behalf`: after one onboarding approval, execute later write requests in the same session without re-asking.",
|
|
528
|
+
"",
|
|
529
|
+
"In operate_on_my_behalf mode, ask only for missing details such as wallet, token, amount, destination, spender, or chain.",
|
|
530
|
+
].join("\n"),
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
uri: "openclawcash://quickstart",
|
|
534
|
+
name: "OpenClawCash MCP Quickstart",
|
|
535
|
+
mimeType: "text/markdown",
|
|
536
|
+
text: [
|
|
537
|
+
"# OpenClawCash MCP Quickstart",
|
|
538
|
+
"",
|
|
539
|
+
"1. Configure `OPENCLAWCASH_AGENT_KEY` or `AGENTWALLETAPI_KEY`.",
|
|
540
|
+
"2. Use `wallets_list` first to discover managed wallets.",
|
|
541
|
+
"3. Use `wallet_get` or `balances_get` before write actions.",
|
|
542
|
+
"4. For writes, establish approval mode once per session.",
|
|
543
|
+
"5. Use `swap_quote` before `swap_execute`.",
|
|
544
|
+
].join("\n"),
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
|
|
548
|
+
function printHelp() {
|
|
549
|
+
const help = [
|
|
550
|
+
"OpenClawCash MCP Server",
|
|
551
|
+
"",
|
|
552
|
+
"Run as stdio MCP server:",
|
|
553
|
+
` npx -y ${PACKAGE_NAME}`,
|
|
554
|
+
"",
|
|
555
|
+
"Optional flags:",
|
|
556
|
+
" --help Show this help",
|
|
557
|
+
" --print-openclaw-config Print an OpenClaw config snippet",
|
|
558
|
+
" --print-claude-config Print a Claude Desktop config snippet",
|
|
559
|
+
" --print-cursor-config Print a Cursor config snippet",
|
|
560
|
+
" --print-vscode-config Print a VS Code config snippet",
|
|
561
|
+
" --self-test Run a local MCP self-test",
|
|
562
|
+
"",
|
|
563
|
+
"Environment variables:",
|
|
564
|
+
" OPENCLAWCASH_AGENT_KEY Preferred agent API key",
|
|
565
|
+
" AGENTWALLETAPI_KEY Backward-compatible agent API key name",
|
|
566
|
+
" OPENCLAWCASH_BASE_URL Optional base URL (default: https://openclawcash.com)",
|
|
567
|
+
" AGENTWALLETAPI_URL Backward-compatible base URL name",
|
|
568
|
+
"",
|
|
569
|
+
"Env file fallback order:",
|
|
570
|
+
" package .env -> cwd .env.local -> cwd .env",
|
|
571
|
+
].join("\n");
|
|
572
|
+
process.stdout.write(`${help}\n`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function printClaudeConfig() {
|
|
576
|
+
const config = {
|
|
577
|
+
mcpServers: {
|
|
578
|
+
openclawcash: {
|
|
579
|
+
command: "npx",
|
|
580
|
+
args: ["-y", PACKAGE_NAME],
|
|
581
|
+
env: {
|
|
582
|
+
OPENCLAWCASH_AGENT_KEY: "occ_your_api_key_here",
|
|
583
|
+
OPENCLAWCASH_BASE_URL: DEFAULT_BASE_URL,
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function printCursorConfig() {
|
|
592
|
+
const config = {
|
|
593
|
+
mcpServers: {
|
|
594
|
+
openclawcash: {
|
|
595
|
+
command: "npx",
|
|
596
|
+
args: ["-y", PACKAGE_NAME],
|
|
597
|
+
env: {
|
|
598
|
+
OPENCLAWCASH_AGENT_KEY: "occ_your_api_key_here",
|
|
599
|
+
OPENCLAWCASH_BASE_URL: DEFAULT_BASE_URL,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function printVSCodeConfig() {
|
|
608
|
+
const config = {
|
|
609
|
+
mcpServers: {
|
|
610
|
+
openclawcash: {
|
|
611
|
+
command: "npx",
|
|
612
|
+
args: ["-y", PACKAGE_NAME],
|
|
613
|
+
env: {
|
|
614
|
+
OPENCLAWCASH_AGENT_KEY: "occ_your_api_key_here",
|
|
615
|
+
OPENCLAWCASH_BASE_URL: DEFAULT_BASE_URL,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function printOpenClawConfig() {
|
|
624
|
+
const config = {
|
|
625
|
+
mcpServers: {
|
|
626
|
+
openclawcash: {
|
|
627
|
+
command: "npx",
|
|
628
|
+
args: ["-y", PACKAGE_NAME],
|
|
629
|
+
env: {
|
|
630
|
+
OPENCLAWCASH_AGENT_KEY: "occ_your_api_key_here",
|
|
631
|
+
OPENCLAWCASH_BASE_URL: DEFAULT_BASE_URL,
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function runSelfTest() {
|
|
640
|
+
const initRequest = {
|
|
641
|
+
jsonrpc: "2.0",
|
|
642
|
+
id: 1,
|
|
643
|
+
method: "initialize",
|
|
644
|
+
params: {
|
|
645
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
646
|
+
capabilities: {},
|
|
647
|
+
clientInfo: {
|
|
648
|
+
name: "openclawcash-self-test",
|
|
649
|
+
version: "0.1.0",
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const toolsListRequest = {
|
|
655
|
+
jsonrpc: "2.0",
|
|
656
|
+
id: 2,
|
|
657
|
+
method: "tools/list",
|
|
658
|
+
params: {},
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
process.stdout.write("Self-test ok\n");
|
|
662
|
+
process.stdout.write(`- protocolVersion: ${PROTOCOL_VERSION}\n`);
|
|
663
|
+
process.stdout.write(`- server: ${SERVER_NAME}@${SERVER_VERSION}\n`);
|
|
664
|
+
process.stdout.write(`- initialize sample: ${JSON.stringify(initRequest)}\n`);
|
|
665
|
+
process.stdout.write(`- tools/list sample: ${JSON.stringify(toolsListRequest)}\n`);
|
|
666
|
+
process.stdout.write(`- configured base URL: ${getBaseUrl()}\n`);
|
|
667
|
+
process.stdout.write(`- agent key present: ${getAgentKey() ? "yes" : "no"}\n`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (process.argv.includes("--help")) {
|
|
671
|
+
printHelp();
|
|
672
|
+
process.exit(0);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (process.argv.includes("--self-test")) {
|
|
676
|
+
await runSelfTest();
|
|
677
|
+
process.exit(0);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (process.argv.includes("--print-claude-config")) {
|
|
681
|
+
printClaudeConfig();
|
|
682
|
+
process.exit(0);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (process.argv.includes("--print-cursor-config")) {
|
|
686
|
+
printCursorConfig();
|
|
687
|
+
process.exit(0);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (process.argv.includes("--print-vscode-config")) {
|
|
691
|
+
printVSCodeConfig();
|
|
692
|
+
process.exit(0);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (process.argv.includes("--print-openclaw-config")) {
|
|
696
|
+
printOpenClawConfig();
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function sendMessage(message) {
|
|
701
|
+
const body = JSON.stringify(message);
|
|
702
|
+
const header = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n`;
|
|
703
|
+
process.stdout.write(header);
|
|
704
|
+
process.stdout.write(body);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function sendResponse(id, result) {
|
|
708
|
+
sendMessage({
|
|
709
|
+
jsonrpc: "2.0",
|
|
710
|
+
id,
|
|
711
|
+
result,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function sendError(id, code, message, data) {
|
|
716
|
+
sendMessage({
|
|
717
|
+
jsonrpc: "2.0",
|
|
718
|
+
id,
|
|
719
|
+
error: {
|
|
720
|
+
code,
|
|
721
|
+
message,
|
|
722
|
+
...(data !== undefined ? { data } : {}),
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function logError(error) {
|
|
728
|
+
const text = error instanceof Error ? `${error.message}\n${error.stack || ""}` : String(error);
|
|
729
|
+
process.stderr.write(`${text}\n`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function handleToolCall(params) {
|
|
733
|
+
const tool = tools.find((candidate) => candidate.name === params?.name);
|
|
734
|
+
if (!tool) {
|
|
735
|
+
return toTextResult(`Unknown tool: ${params?.name || "<missing>"}`, true);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
try {
|
|
739
|
+
const parsedArgs = tool.parse(params.arguments || {});
|
|
740
|
+
const data = await tool.execute(parsedArgs);
|
|
741
|
+
return toTextResult(data);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
if (error instanceof z.ZodError) {
|
|
744
|
+
return toTextResult(
|
|
745
|
+
{
|
|
746
|
+
message: "Invalid tool arguments",
|
|
747
|
+
issues: error.issues.map((issue) => ({
|
|
748
|
+
path: issue.path.join("."),
|
|
749
|
+
message: issue.message,
|
|
750
|
+
})),
|
|
751
|
+
},
|
|
752
|
+
true,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return toTextResult(
|
|
757
|
+
{
|
|
758
|
+
message: error instanceof Error ? error.message : String(error),
|
|
759
|
+
status: error?.status,
|
|
760
|
+
payload: error?.payload,
|
|
761
|
+
},
|
|
762
|
+
true,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function handleRequest(message) {
|
|
768
|
+
const { id, method, params } = message;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
switch (method) {
|
|
772
|
+
case "initialize":
|
|
773
|
+
sendResponse(id, {
|
|
774
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
775
|
+
capabilities: {
|
|
776
|
+
tools: {},
|
|
777
|
+
resources: {},
|
|
778
|
+
},
|
|
779
|
+
serverInfo: {
|
|
780
|
+
name: SERVER_NAME,
|
|
781
|
+
version: SERVER_VERSION,
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
return;
|
|
785
|
+
|
|
786
|
+
case "notifications/initialized":
|
|
787
|
+
return;
|
|
788
|
+
|
|
789
|
+
case "ping":
|
|
790
|
+
sendResponse(id, {});
|
|
791
|
+
return;
|
|
792
|
+
|
|
793
|
+
case "tools/list":
|
|
794
|
+
sendResponse(id, {
|
|
795
|
+
tools: tools.map((tool) => ({
|
|
796
|
+
name: tool.name,
|
|
797
|
+
description: tool.description,
|
|
798
|
+
inputSchema: tool.inputSchema,
|
|
799
|
+
})),
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
|
|
803
|
+
case "tools/call":
|
|
804
|
+
sendResponse(id, await handleToolCall(params));
|
|
805
|
+
return;
|
|
806
|
+
|
|
807
|
+
case "resources/list":
|
|
808
|
+
sendResponse(id, {
|
|
809
|
+
resources: resources.map((resource) => ({
|
|
810
|
+
uri: resource.uri,
|
|
811
|
+
name: resource.name,
|
|
812
|
+
mimeType: resource.mimeType,
|
|
813
|
+
})),
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
|
|
817
|
+
case "resources/read": {
|
|
818
|
+
const resource = resources.find((candidate) => candidate.uri === params?.uri);
|
|
819
|
+
if (!resource) {
|
|
820
|
+
sendError(id, -32002, `Unknown resource: ${params?.uri || "<missing>"}`);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
sendResponse(id, {
|
|
824
|
+
contents: [
|
|
825
|
+
{
|
|
826
|
+
uri: resource.uri,
|
|
827
|
+
mimeType: resource.mimeType,
|
|
828
|
+
text: resource.text,
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
});
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
default:
|
|
836
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
837
|
+
}
|
|
838
|
+
} catch (error) {
|
|
839
|
+
logError(error);
|
|
840
|
+
sendError(id, -32603, error instanceof Error ? error.message : String(error));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
let inputBuffer = Buffer.alloc(0);
|
|
845
|
+
|
|
846
|
+
function processBuffer() {
|
|
847
|
+
while (true) {
|
|
848
|
+
const headerEnd = inputBuffer.indexOf("\r\n\r\n");
|
|
849
|
+
if (headerEnd === -1) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const headerText = inputBuffer.subarray(0, headerEnd).toString("utf8");
|
|
854
|
+
const lengthMatch = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
855
|
+
if (!lengthMatch) {
|
|
856
|
+
throw new Error("Invalid MCP message: missing Content-Length header.");
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const contentLength = Number(lengthMatch[1]);
|
|
860
|
+
const totalLength = headerEnd + 4 + contentLength;
|
|
861
|
+
if (inputBuffer.length < totalLength) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const body = inputBuffer.subarray(headerEnd + 4, totalLength).toString("utf8");
|
|
866
|
+
inputBuffer = inputBuffer.subarray(totalLength);
|
|
867
|
+
|
|
868
|
+
const message = JSON.parse(body);
|
|
869
|
+
if (message && typeof message.method === "string") {
|
|
870
|
+
void handleRequest(message);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
process.stdin.on("data", (chunk) => {
|
|
876
|
+
inputBuffer = Buffer.concat([inputBuffer, chunk]);
|
|
877
|
+
try {
|
|
878
|
+
processBuffer();
|
|
879
|
+
} catch (error) {
|
|
880
|
+
logError(error);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
process.stdin.on("error", (error) => {
|
|
885
|
+
logError(error);
|
|
886
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openclawcash/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClawCash MCP server for managed agent wallets, balances, transfers, swaps, and approvals.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Proprietary",
|
|
7
|
+
"homepage": "https://openclawcash.com/mcp",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://openclawcash.com/mcp"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"author": "OpenClawCash",
|
|
15
|
+
"bin": {
|
|
16
|
+
"openclawcash-mcp": "./openclawcash-mcp.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"openclawcash-mcp.mjs",
|
|
20
|
+
"README.md",
|
|
21
|
+
".env.example"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"openclawcash",
|
|
27
|
+
"crypto",
|
|
28
|
+
"wallet",
|
|
29
|
+
"agents"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"zod": "^3.24.2"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"start": "node ./openclawcash-mcp.mjs",
|
|
39
|
+
"self-test": "node ./openclawcash-mcp.mjs --self-test",
|
|
40
|
+
"print:openclaw": "node ./openclawcash-mcp.mjs --print-openclaw-config",
|
|
41
|
+
"print:claude": "node ./openclawcash-mcp.mjs --print-claude-config",
|
|
42
|
+
"print:cursor": "node ./openclawcash-mcp.mjs --print-cursor-config",
|
|
43
|
+
"print:vscode": "node ./openclawcash-mcp.mjs --print-vscode-config"
|
|
44
|
+
}
|
|
45
|
+
}
|