@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 ADDED
@@ -0,0 +1,2 @@
1
+ OPENCLAWCASH_AGENT_KEY=occ_your_api_key_here
2
+ OPENCLAWCASH_BASE_URL=https://openclawcash.com
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
+ }