@abstract-foundation/agw-mcp 0.1.0-beta.3 → 0.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -12
- package/dist/index.mjs +66 -12
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -9,13 +9,13 @@ MCP server for [Abstract Global Wallet](https://abs.xyz) session-key workflows
|
|
|
9
9
|
## Quick Start
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y @abstract-foundation/agw-mcp serve --chain-id
|
|
12
|
+
npx -y @abstract-foundation/agw-mcp serve --chain-id 2741
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Or add it to Claude Code directly:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
claude mcp add agw -- npx -y @abstract-foundation/agw-mcp serve --chain-id
|
|
18
|
+
claude mcp add agw -- npx -y @abstract-foundation/agw-mcp serve --chain-id 2741
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Setup
|
|
@@ -23,10 +23,10 @@ claude mcp add agw -- npx -y @abstract-foundation/agw-mcp serve --chain-id 11124
|
|
|
23
23
|
### 1. Bootstrap a session
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx -y @abstract-foundation/agw-mcp init --chain-id
|
|
26
|
+
npx -y @abstract-foundation/agw-mcp init --chain-id 2741
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
This opens the hosted onboarding app (`https://
|
|
29
|
+
This opens the hosted onboarding app (`https://mcp.abs.xyz` by default) where you:
|
|
30
30
|
|
|
31
31
|
1. Choose a policy preset (or provide custom policy JSON)
|
|
32
32
|
2. Connect your Abstract Global Wallet
|
|
@@ -35,11 +35,12 @@ This opens the hosted onboarding app (`https://app-jarrodwatts.vercel.app`) wher
|
|
|
35
35
|
Session data is saved to `~/.agw-mcp/session.json` with `0o600` file permissions. The session signer key is stored separately in `~/.agw-mcp/session-signer.key`.
|
|
36
36
|
If a previous active session exists locally, the CLI attempts to revoke it on-chain after creating the new one.
|
|
37
37
|
Bootstrap is single-process per storage directory (lockfile: `~/.agw-mcp/.bootstrap-init.lock`) to prevent concurrent `init` races.
|
|
38
|
+
When local sessions are revoked/cleared, the signer keyfile is deleted as part of local cleanup.
|
|
38
39
|
|
|
39
40
|
### 2. Start the MCP server
|
|
40
41
|
|
|
41
42
|
```bash
|
|
42
|
-
npx -y @abstract-foundation/agw-mcp serve --chain-id
|
|
43
|
+
npx -y @abstract-foundation/agw-mcp serve --chain-id 2741
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
## Client Configuration
|
|
@@ -47,7 +48,7 @@ npx -y @abstract-foundation/agw-mcp serve --chain-id 11124
|
|
|
47
48
|
### Claude Code
|
|
48
49
|
|
|
49
50
|
```bash
|
|
50
|
-
claude mcp add agw -- npx -y @abstract-foundation/agw-mcp serve --chain-id
|
|
51
|
+
claude mcp add agw -- npx -y @abstract-foundation/agw-mcp serve --chain-id 2741
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
### Claude Desktop
|
|
@@ -62,7 +63,7 @@ Add to your `claude_desktop_config.json`:
|
|
|
62
63
|
"mcpServers": {
|
|
63
64
|
"agw-mcp": {
|
|
64
65
|
"command": "npx",
|
|
65
|
-
"args": ["-y", "@abstract-foundation/agw-mcp", "serve", "--chain-id", "
|
|
66
|
+
"args": ["-y", "@abstract-foundation/agw-mcp", "serve", "--chain-id", "2741"]
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -78,7 +79,7 @@ Add to your `claude_desktop_config.json`:
|
|
|
78
79
|
"mcpServers": {
|
|
79
80
|
"agw-mcp": {
|
|
80
81
|
"command": "npx",
|
|
81
|
-
"args": ["-y", "@abstract-foundation/agw-mcp", "serve", "--chain-id", "
|
|
82
|
+
"args": ["-y", "@abstract-foundation/agw-mcp", "serve", "--chain-id", "2741"]
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -93,7 +94,7 @@ Use the same JSON block as Claude Desktop in your editor's MCP configuration fil
|
|
|
93
94
|
### Generate config snippet
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
npx -y @abstract-foundation/agw-mcp config --npx --chain-id
|
|
97
|
+
npx -y @abstract-foundation/agw-mcp config --npx --chain-id 2741
|
|
97
98
|
```
|
|
98
99
|
|
|
99
100
|
## Tools
|
|
@@ -117,7 +118,7 @@ npx -y @abstract-foundation/agw-mcp config --npx --chain-id 11124
|
|
|
117
118
|
|
|
118
119
|
## Network Configuration
|
|
119
120
|
|
|
120
|
-
Defaults to Abstract
|
|
121
|
+
Defaults to Abstract mainnet (chain ID `2741`). Override RPC or switch to testnet when needed:
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
124
|
# Mainnet
|
|
@@ -132,15 +133,16 @@ Environment variables are also supported:
|
|
|
132
133
|
```bash
|
|
133
134
|
AGW_MCP_CHAIN_ID=2741 npx -y @abstract-foundation/agw-mcp serve
|
|
134
135
|
AGW_MCP_RPC_URL=https://api.mainnet.abs.xyz npx -y @abstract-foundation/agw-mcp serve
|
|
135
|
-
AGW_MCP_APP_URL=
|
|
136
|
+
AGW_MCP_APP_URL=https://mcp.abs.xyz npx -y @abstract-foundation/agw-mcp init --chain-id 2741
|
|
136
137
|
```
|
|
137
138
|
|
|
138
139
|
`init` requires `https://` app URLs except for loopback local development URLs (`http://localhost`, `http://127.0.0.1`, `http://[::1]`).
|
|
140
|
+
`init` defaults to `https://mcp.abs.xyz` if no app URL is configured via `--app-url` or `AGW_MCP_APP_URL`.
|
|
139
141
|
|
|
140
142
|
For local hosted-app development:
|
|
141
143
|
|
|
142
144
|
```bash
|
|
143
|
-
npx -y @abstract-foundation/agw-mcp init --chain-id
|
|
145
|
+
npx -y @abstract-foundation/agw-mcp init --chain-id 2741 --app-url http://localhost:3001
|
|
144
146
|
```
|
|
145
147
|
|
|
146
148
|
## Security Model
|
|
@@ -151,6 +153,15 @@ npx -y @abstract-foundation/agw-mcp init --chain-id 11124 --app-url http://local
|
|
|
151
153
|
- **Restrictive file permissions**: Session storage directory `0o700`, files `0o600`.
|
|
152
154
|
- **Stderr-only logging**: stdout is reserved for MCP stdio transport. All operational logs go to stderr.
|
|
153
155
|
|
|
156
|
+
### Real Funds Checklist
|
|
157
|
+
|
|
158
|
+
For production usage with real money:
|
|
159
|
+
|
|
160
|
+
1. Use a trusted onboarding host (`--app-url` or `AGW_MCP_APP_URL`) and pin it in deployment config.
|
|
161
|
+
2. Start with minimal intent scope (prefer payments-only) and shortest practical expiry.
|
|
162
|
+
3. Keep `execute` off by default and run preview-first workflows where possible.
|
|
163
|
+
4. Revoke sessions after task completion (`revoke_session`) and confirm status with `get_session_status`.
|
|
164
|
+
|
|
154
165
|
## Development
|
|
155
166
|
|
|
156
167
|
```bash
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import open from "open";
|
|
4
5
|
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
6
|
-
import fs from "node:fs";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { sessionKeyValidatorAddress } from "@abstract-foundation/agw-client/constants";
|
|
@@ -117,12 +117,24 @@ var SessionStorage = class {
|
|
|
117
117
|
}
|
|
118
118
|
resolveSignerRefForRuntime(data) {
|
|
119
119
|
if (data.sessionSignerRef.kind === "keyfile") {
|
|
120
|
-
if (!fs.existsSync(data.sessionSignerRef.value))
|
|
120
|
+
if (!fs.existsSync(data.sessionSignerRef.value)) {
|
|
121
|
+
if (data.status === "revoked") return {
|
|
122
|
+
...data,
|
|
123
|
+
sessionSignerRef: {
|
|
124
|
+
kind: "raw",
|
|
125
|
+
value: REDACTED_SIGNER_REF$1
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
121
130
|
return data;
|
|
122
131
|
}
|
|
123
132
|
const rawValue = data.sessionSignerRef.value.trim();
|
|
124
133
|
if (rawValue === REDACTED_SIGNER_REF$1) {
|
|
125
|
-
if (!fs.existsSync(this.signerKeyPath))
|
|
134
|
+
if (!fs.existsSync(this.signerKeyPath)) {
|
|
135
|
+
if (data.status === "revoked") return data;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
126
138
|
return {
|
|
127
139
|
...data,
|
|
128
140
|
sessionSignerRef: {
|
|
@@ -131,7 +143,16 @@ var SessionStorage = class {
|
|
|
131
143
|
}
|
|
132
144
|
};
|
|
133
145
|
}
|
|
134
|
-
if (!PRIVATE_KEY_PATTERN$4.test(rawValue))
|
|
146
|
+
if (!PRIVATE_KEY_PATTERN$4.test(rawValue)) {
|
|
147
|
+
if (data.status === "revoked") return {
|
|
148
|
+
...data,
|
|
149
|
+
sessionSignerRef: {
|
|
150
|
+
kind: "raw",
|
|
151
|
+
value: REDACTED_SIGNER_REF$1
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
135
156
|
return {
|
|
136
157
|
...data,
|
|
137
158
|
sessionSignerRef: {
|
|
@@ -172,11 +193,23 @@ var SessionStorage = class {
|
|
|
172
193
|
} : data;
|
|
173
194
|
fs.writeFileSync(this.filePath, JSON.stringify(this.sanitizeForPersistence(normalized), null, 2), { mode: 384 });
|
|
174
195
|
}
|
|
175
|
-
|
|
196
|
+
deleteFile(filePath, wipe = false) {
|
|
176
197
|
try {
|
|
177
|
-
if (fs.existsSync(
|
|
198
|
+
if (!fs.existsSync(filePath)) return;
|
|
199
|
+
if (wipe) try {
|
|
200
|
+
const stats = fs.statSync(filePath);
|
|
201
|
+
if (stats.isFile() && stats.size > 0) fs.writeFileSync(filePath, Buffer.alloc(stats.size), { flag: "r+" });
|
|
202
|
+
} catch {}
|
|
203
|
+
fs.unlinkSync(filePath);
|
|
178
204
|
} catch {}
|
|
179
205
|
}
|
|
206
|
+
deleteSignerKeyfile() {
|
|
207
|
+
this.deleteFile(this.signerKeyPath, true);
|
|
208
|
+
}
|
|
209
|
+
delete() {
|
|
210
|
+
this.deleteFile(this.filePath);
|
|
211
|
+
this.deleteSignerKeyfile();
|
|
212
|
+
}
|
|
180
213
|
};
|
|
181
214
|
|
|
182
215
|
//#endregion
|
|
@@ -206,7 +239,7 @@ const SUPPORTED_CHAINS = {
|
|
|
206
239
|
[abstractTestnet.id]: abstractTestnet,
|
|
207
240
|
[abstract.id]: abstract
|
|
208
241
|
};
|
|
209
|
-
const DEFAULT_CHAIN_ID =
|
|
242
|
+
const DEFAULT_CHAIN_ID = abstract.id;
|
|
210
243
|
function normalizeOptionalString(value) {
|
|
211
244
|
if (typeof value !== "string") return;
|
|
212
245
|
const normalized = value.trim();
|
|
@@ -891,6 +924,7 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
891
924
|
"[::1]"
|
|
892
925
|
]);
|
|
893
926
|
const BOOTSTRAP_LOCK_STALE_MS = 1800 * 1e3;
|
|
927
|
+
const DEFAULT_ONBOARDING_APP_URL = "https://mcp.abs.xyz";
|
|
894
928
|
function ensurePrivateDir$1(dir) {
|
|
895
929
|
fs.mkdirSync(dir, {
|
|
896
930
|
recursive: true,
|
|
@@ -1019,7 +1053,11 @@ function isLoopbackHostname(hostname) {
|
|
|
1019
1053
|
return LOOPBACK_HOSTS.has(hostname);
|
|
1020
1054
|
}
|
|
1021
1055
|
function resolveAppUrl(options) {
|
|
1022
|
-
|
|
1056
|
+
const explicitAppUrl = options.appUrl?.trim();
|
|
1057
|
+
if (explicitAppUrl) return explicitAppUrl;
|
|
1058
|
+
const envAppUrl = process.env.AGW_MCP_APP_URL?.trim();
|
|
1059
|
+
if (envAppUrl) return envAppUrl;
|
|
1060
|
+
return DEFAULT_ONBOARDING_APP_URL;
|
|
1023
1061
|
}
|
|
1024
1062
|
function validateAppUrl(appUrl) {
|
|
1025
1063
|
let appOrigin;
|
|
@@ -1406,7 +1444,7 @@ var AuditLog = class {
|
|
|
1406
1444
|
const ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
|
|
1407
1445
|
const SELECTOR_PATTERN = /^0x[0-9a-fA-F]{8}$/;
|
|
1408
1446
|
const UINT_PATTERN = /^\d+$/;
|
|
1409
|
-
const DEFAULT_MAX_SESSION_LIFETIME_SECONDS =
|
|
1447
|
+
const DEFAULT_MAX_SESSION_LIFETIME_SECONDS = 720 * 60 * 60;
|
|
1410
1448
|
const EQUAL_CONDITION = 1n;
|
|
1411
1449
|
const UNSAFE_DESTINATION_SELECTORS = new Set([
|
|
1412
1450
|
"0xa22cb465",
|
|
@@ -1644,7 +1682,7 @@ var SessionManager = class {
|
|
|
1644
1682
|
constructor(logger, options = {}) {
|
|
1645
1683
|
this.logger = logger.child("session");
|
|
1646
1684
|
this.storage = new SessionStorage(options.storageDir);
|
|
1647
|
-
this.chainId = options.chainId ??
|
|
1685
|
+
this.chainId = options.chainId ?? DEFAULT_CHAIN_ID;
|
|
1648
1686
|
this.rpcUrl = options.rpcUrl;
|
|
1649
1687
|
}
|
|
1650
1688
|
initialize() {
|
|
@@ -1678,6 +1716,12 @@ var SessionManager = class {
|
|
|
1678
1716
|
source: "local",
|
|
1679
1717
|
checkedAt
|
|
1680
1718
|
};
|
|
1719
|
+
if (this.session.status === "revoked") return {
|
|
1720
|
+
status: "Closed",
|
|
1721
|
+
statusCode: 2,
|
|
1722
|
+
source: "local",
|
|
1723
|
+
checkedAt
|
|
1724
|
+
};
|
|
1681
1725
|
const networkConfig = this.getNetworkConfig(this.session.chainId);
|
|
1682
1726
|
const sessionClient = createSessionClientFromSessionData({
|
|
1683
1727
|
session: this.session,
|
|
@@ -1714,6 +1758,7 @@ var SessionManager = class {
|
|
|
1714
1758
|
updatedAt: updatedAtUnixSeconds
|
|
1715
1759
|
};
|
|
1716
1760
|
this.storage.save(this.session);
|
|
1761
|
+
this.storage.deleteSignerKeyfile();
|
|
1717
1762
|
}
|
|
1718
1763
|
getChainId() {
|
|
1719
1764
|
return this.chainId;
|
|
@@ -3313,9 +3358,18 @@ var AgwMcpServer = class {
|
|
|
3313
3358
|
//#endregion
|
|
3314
3359
|
//#region src/index.ts
|
|
3315
3360
|
const logger = new Logger("agw-mcp");
|
|
3361
|
+
function resolveCliVersion() {
|
|
3362
|
+
try {
|
|
3363
|
+
const packageJsonUrl = new URL("../package.json", import.meta.url);
|
|
3364
|
+
const packageJsonRaw = fs.readFileSync(packageJsonUrl, "utf8");
|
|
3365
|
+
const parsed = JSON.parse(packageJsonRaw);
|
|
3366
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) return parsed.version.trim();
|
|
3367
|
+
} catch {}
|
|
3368
|
+
return "0.1.0";
|
|
3369
|
+
}
|
|
3316
3370
|
const program = new Command();
|
|
3317
|
-
program.name("agw-mcp").description("Local MCP server for AGW session-key workflows").version(
|
|
3318
|
-
program.command("init").description("Bootstrap local AGW MCP session storage").option("--chain-id <chainId>", "EVM chain id (env: AGW_MCP_CHAIN_ID)").option("--rpc-url <rpcUrl>", "RPC URL override (env: AGW_MCP_RPC_URL)").option("--app-url <url>", "Hosted session onboarding URL (env: AGW_MCP_APP_URL)").option("--storage-dir <dir>", "Session storage directory").action(async (options) => {
|
|
3371
|
+
program.name("agw-mcp").description("Local MCP server for AGW session-key workflows").version(resolveCliVersion());
|
|
3372
|
+
program.command("init").description("Bootstrap local AGW MCP session storage").option("--chain-id <chainId>", "EVM chain id (env: AGW_MCP_CHAIN_ID)").option("--rpc-url <rpcUrl>", "RPC URL override (env: AGW_MCP_RPC_URL)").option("--app-url <url>", "Hosted session onboarding URL (defaults to https://mcp.abs.xyz; env: AGW_MCP_APP_URL)").option("--storage-dir <dir>", "Session storage directory").action(async (options) => {
|
|
3319
3373
|
const networkConfig = resolveNetworkConfig({
|
|
3320
3374
|
chainId: options.chainId,
|
|
3321
3375
|
rpcUrl: options.rpcUrl
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abstract-foundation/agw-mcp",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.6",
|
|
4
4
|
"description": "MCP server for Abstract Global Wallet session-key workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Abstract Foundation",
|
|
@@ -71,5 +71,11 @@
|
|
|
71
71
|
"tsdown": "^0.20.3",
|
|
72
72
|
"tsx": "^4.19.3",
|
|
73
73
|
"typescript": "^5.8.3"
|
|
74
|
+
},
|
|
75
|
+
"pnpm": {
|
|
76
|
+
"overrides": {
|
|
77
|
+
"ajv": "6.14.0",
|
|
78
|
+
"minimatch": "10.2.1"
|
|
79
|
+
}
|
|
74
80
|
}
|
|
75
81
|
}
|