@goplausible/algorand-mcp 4.2.8 → 4.3.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/README.md +41 -40
- package/dist/tools/utilityManager.js +1 -1
- package/dist/tools/walletManager.d.ts +1 -0
- package/dist/tools/walletManager.js +85 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
-
|
|
16
|
+
- Agent wallet — mnemonics stored in a local SQLite database, used by the MCP server to sign on the agent's behalf (mnemonics never returned in tool responses)
|
|
17
17
|
- Wallet accounts with human-readable nicknames
|
|
18
18
|
- Account creation, key management, and rekeying
|
|
19
19
|
- Transaction building, signing, and submission (payments, assets, applications, key registration)
|
|
@@ -268,57 +268,58 @@ If no `network` is provided, tools default to **mainnet**.
|
|
|
268
268
|
|
|
269
269
|
API responses are automatically paginated. Every tool accepts an optional `itemsPerPage` parameter (default: 10). Pass the `pageToken` from a previous response to fetch the next page.
|
|
270
270
|
|
|
271
|
-
##
|
|
271
|
+
## Agent Wallet
|
|
272
272
|
|
|
273
273
|
### Architecture
|
|
274
274
|
|
|
275
|
-
The wallet
|
|
275
|
+
The agent wallet is a local SQLite database that the MCP server controls on the agent's behalf. The server holds the mnemonics and signs transactions for the agent — the agent never sees the mnemonics in any tool response.
|
|
276
276
|
|
|
277
|
-
| Layer | What it stores | Where |
|
|
278
|
-
|
|
279
|
-
| **
|
|
280
|
-
| **Embedded SQLite** | Account metadata (nicknames, active-account index) | `~/.algorand-mcp/wallet.db` | Plaintext (no secrets) |
|
|
277
|
+
| Layer | What it stores | Where |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| **SQLite (`wallet.db`)** | Account rows (`address`, `public_key`, `nickname`, `mnemonic`, `created_at`) and the active-account index | `~/.algorand-mcp/wallet.db` (mode `0600`) |
|
|
281
280
|
|
|
282
|
-
**
|
|
281
|
+
**Threat model.** The wallet.db file is the secret. Anyone with read access to it can recover every mnemonic stored in the wallet. The mitigations are filesystem permissions (`0600`, owner-only), keeping the data directory off shared/world-readable volumes, and treating the data directory like any other secret store (snapshot it carefully, restrict backups, encrypt the host disk for at-rest protection). For Docker deployments, mount `~/.algorand-mcp` as a named volume and restrict access to it like you would any secret material.
|
|
283
282
|
|
|
284
283
|
### How it works
|
|
285
284
|
|
|
286
285
|
```
|
|
287
|
-
Agent (LLM) MCP Server
|
|
288
|
-
────────── ──────────
|
|
289
|
-
│ │
|
|
290
|
-
│ wallet_add_account │
|
|
291
|
-
│ { nickname: "main" } │
|
|
292
|
-
│ ──────────────────────────► │ generate keypair
|
|
293
|
-
│ │
|
|
294
|
-
│ │
|
|
295
|
-
│ ◄─ { address, publicKey
|
|
296
|
-
│
|
|
297
|
-
│
|
|
298
|
-
│
|
|
299
|
-
│
|
|
300
|
-
│
|
|
301
|
-
│
|
|
302
|
-
│ │
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
286
|
+
Agent (LLM) MCP Server Storage
|
|
287
|
+
────────── ────────── ───────
|
|
288
|
+
│ │ │
|
|
289
|
+
│ wallet_add_account │ │
|
|
290
|
+
│ { nickname: "main" } │ │
|
|
291
|
+
│ ──────────────────────────► │ generate keypair │
|
|
292
|
+
│ │ INSERT (address, public_key, │
|
|
293
|
+
│ │ nickname, mnemonic) ──►│ wallet.db
|
|
294
|
+
│ ◄─ { address, publicKey, │ │
|
|
295
|
+
│ nickname, index } │ │
|
|
296
|
+
│ │ │
|
|
297
|
+
│ wallet_sign_transaction │ │
|
|
298
|
+
│ { transaction: {...} } │ │
|
|
299
|
+
│ ──────────────────────────► │ SELECT mnemonic FROM accounts ◄─│
|
|
300
|
+
│ │ WHERE address=<active> │
|
|
301
|
+
│ │ sign in memory │
|
|
302
|
+
│ ◄─ { txID, blob } │ (key discarded after sign) │
|
|
303
|
+
│ │ │
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
1. **Account creation** (`wallet_add_account`) — Generates a keypair and inserts a row containing the mnemonic into `accounts`. Returns address, public key, nickname, and index. The mnemonic is never returned.
|
|
306
307
|
2. **Active account** — One account is active at a time. `wallet_switch_account` changes it by nickname or index. All signing and query tools operate on the active account.
|
|
307
|
-
3. **Transaction signing** (`wallet_sign_transaction`) —
|
|
308
|
+
3. **Transaction signing** (`wallet_sign_transaction`) — Reads the mnemonic from the DB, signs in memory, returns only the signed blob.
|
|
308
309
|
4. **Data signing** (`wallet_sign_data`) — Signs arbitrary hex data using raw Ed25519 via the [`@noble/curves`](https://github.com/paulmillr/noble-curves) library (no Algorand SDK prefix). Useful for off-chain authentication.
|
|
309
310
|
5. **Asset opt-in** (`wallet_optin_asset`) — Creates, signs, and submits an opt-in transaction for the active account in one step.
|
|
310
311
|
|
|
311
|
-
###
|
|
312
|
+
### Backward compatibility (silent migration from OS keychain)
|
|
312
313
|
|
|
313
|
-
|
|
314
|
+
Older installs of this MCP stored mnemonics in the OS keychain (`@napi-rs/keyring`). On first startup after upgrading, the server runs a one-shot, silent migration:
|
|
314
315
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
- For every `accounts` row whose `mnemonic` column is `NULL` or empty, it attempts to read the mnemonic from the OS keychain under the service name `algorand-mcp` keyed by the address.
|
|
317
|
+
- If found, the mnemonic is copied into the DB column.
|
|
318
|
+
- The original keychain entry is left in place as a redundant backup; nothing is deleted.
|
|
319
|
+
|
|
320
|
+
After this completes, the DB is the sole source of truth. The keychain is consulted only as a fallback if the DB still has a `NULL` mnemonic for an address (e.g., the keychain was unavailable during startup and became available later). All new accounts created after the upgrade are written directly to the DB and never touch the keychain.
|
|
320
321
|
|
|
321
|
-
|
|
322
|
+
No user action is required. No prompts, no env vars, no migration logs.
|
|
322
323
|
|
|
323
324
|
## x402 HTTP Payments
|
|
324
325
|
|
|
@@ -343,7 +344,7 @@ This MCP implements the Algorand flavor of x402, where payments are USDC (or nat
|
|
|
343
344
|
│ │ build fee-payer + payment │ │
|
|
344
345
|
│ │ (atomic group of 2) │ │
|
|
345
346
|
│ │ sign payment leg │ │
|
|
346
|
-
│ │ via
|
|
347
|
+
│ │ via agent wallet DB │ │
|
|
347
348
|
│ │ encode unsigned fee-payer │ │
|
|
348
349
|
│ │ base64 PAYMENT-SIGNATURE │ │
|
|
349
350
|
│ │ ─────────────────────────► │ │
|
|
@@ -452,7 +453,7 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
452
453
|
| `wallet_remove_account` | Remove an account from the wallet by nickname or index |
|
|
453
454
|
| `wallet_list_accounts` | List all accounts with nicknames and addresses |
|
|
454
455
|
| `wallet_switch_account` | Switch the active account by nickname or index |
|
|
455
|
-
| `wallet_get_info` | Get info for the **active account this MCP server owns** (
|
|
456
|
+
| `wallet_get_info` | Get info for the **active account this MCP server owns** (DB-backed): address, public key, balance, opted-in counts. For arbitrary on-chain accounts use `api_algod_get_account_info`. |
|
|
456
457
|
| `wallet_get_assets` | Get all ASA holdings for the **active account this MCP server owns**. For arbitrary on-chain accounts use `api_algod_get_account_info` or `api_algod_get_account_asset_info`. |
|
|
457
458
|
| `wallet_sign_transaction` | Sign a single transaction with the active account |
|
|
458
459
|
| `wallet_sign_transaction_group` | Sign a group of transactions with the active account (auto-assigns group ID) |
|
|
@@ -687,7 +688,7 @@ algorand-mcp/
|
|
|
687
688
|
│ │ └── wallet/ # Wallet resources
|
|
688
689
|
│ ├── tools/ # MCP Tools
|
|
689
690
|
│ │ ├── commonParams.ts # Network + pagination schema fragments
|
|
690
|
-
│ │ ├── walletManager.ts #
|
|
691
|
+
│ │ ├── walletManager.ts # Agent wallet (SQLite-backed)
|
|
691
692
|
│ │ ├── accountManager.ts # Account operations
|
|
692
693
|
│ │ ├── utilityManager.ts # Utility functions
|
|
693
694
|
│ │ ├── algodManager.ts # TEAL compile, simulate, submit
|
|
@@ -967,7 +968,7 @@ describeIf(testConfig.isCategoryEnabled('your-category'))('Your Tools (E2E)', ()
|
|
|
967
968
|
|
|
968
969
|
- [algosdk](https://github.com/algorand/js-algorand-sdk) v3 — Algorand JavaScript SDK
|
|
969
970
|
- [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) — MCP TypeScript SDK
|
|
970
|
-
- [@napi-rs/keyring](https://github.com/nicolo-ribaudo/napi-keyring) — Native OS keychain access (macOS Keychain, Linux libsecret, Windows Credential Manager)
|
|
971
|
+
- [@napi-rs/keyring](https://github.com/nicolo-ribaudo/napi-keyring) — Native OS keychain access (macOS Keychain, Linux libsecret, Windows Credential Manager). Used as a backward-compatibility read fallback for accounts created by pre-DB-migration installs; new mnemonics are written only to `wallet.db`.
|
|
971
972
|
- [sql.js](https://github.com/sql-js/sql.js) — Embedded SQLite (WASM) for wallet metadata persistence
|
|
972
973
|
- [@noble/curves](https://github.com/paulmillr/noble-curves) — Pure JS Ed25519 for raw data signing (`wallet_sign_data`)
|
|
973
974
|
- [@tinymanorg/tinyman-js-sdk](https://github.com/tinymanorg/tinyman-js-sdk) — Tinyman AMM SDK
|
|
@@ -86,7 +86,7 @@ export class UtilityManager {
|
|
|
86
86
|
type: 'text',
|
|
87
87
|
text: JSON.stringify({
|
|
88
88
|
name: 'Algorand MCP Server',
|
|
89
|
-
version: '4.
|
|
89
|
+
version: '4.3.0',
|
|
90
90
|
builder: 'GoPlausible',
|
|
91
91
|
description: 'A Model Context Protocol (MCP) server providing comprehensive access to the Algorand blockchain. Supports account management, transaction building and signing, smart contract interaction, asset operations, ARC-26 URI generation, and deep integration with Algorand ecosystem services including NFDomains, Tinyman, Vestige, and Ultrade.',
|
|
92
92
|
blockchain: 'Algorand — a carbon-negative, pure proof-of-stake Layer 1 blockchain delivering instant finality, low fees, and advanced smart contract capabilities via AVM (Algorand Virtual Machine).',
|
|
@@ -5,7 +5,7 @@ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
|
5
5
|
import { withCommonParams } from './commonParams.js';
|
|
6
6
|
import { getAlgodClient, extractNetwork } from '../algorand-client.js';
|
|
7
7
|
import initSqlJs from 'sql.js';
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -35,6 +35,7 @@ async function getDb() {
|
|
|
35
35
|
address TEXT UNIQUE NOT NULL,
|
|
36
36
|
public_key TEXT NOT NULL,
|
|
37
37
|
nickname TEXT UNIQUE NOT NULL,
|
|
38
|
+
mnemonic TEXT,
|
|
38
39
|
created_at TEXT NOT NULL
|
|
39
40
|
)
|
|
40
41
|
`);
|
|
@@ -44,19 +45,56 @@ async function getDb() {
|
|
|
44
45
|
value TEXT NOT NULL
|
|
45
46
|
)
|
|
46
47
|
`);
|
|
48
|
+
// Idempotent schema migration: ensure `mnemonic` column exists on pre-existing DBs.
|
|
49
|
+
ensureMnemonicColumn(db);
|
|
47
50
|
// Initialize active index if not set
|
|
48
51
|
const row = db.exec("SELECT value FROM wallet_state WHERE key = 'active_account_index'");
|
|
49
52
|
if (row.length === 0) {
|
|
50
53
|
db.run("INSERT INTO wallet_state (key, value) VALUES ('active_account_index', '0')");
|
|
51
54
|
}
|
|
55
|
+
// Eager keychain → DB migration for any pre-existing accounts whose mnemonic
|
|
56
|
+
// still lives in the OS keychain. Runs once per startup; idempotent (rows
|
|
57
|
+
// already populated are skipped).
|
|
58
|
+
migrateKeychainToDb(db);
|
|
52
59
|
persistDb();
|
|
53
60
|
return db;
|
|
54
61
|
}
|
|
62
|
+
function ensureMnemonicColumn(database) {
|
|
63
|
+
const info = database.exec('PRAGMA table_info(accounts)');
|
|
64
|
+
if (!info.length)
|
|
65
|
+
return;
|
|
66
|
+
const cols = info[0].values.map(r => r[1]);
|
|
67
|
+
if (!cols.includes('mnemonic')) {
|
|
68
|
+
database.run('ALTER TABLE accounts ADD COLUMN mnemonic TEXT');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function migrateKeychainToDb(database) {
|
|
72
|
+
const result = database.exec("SELECT address FROM accounts WHERE mnemonic IS NULL OR mnemonic = ''");
|
|
73
|
+
if (!result.length || !result[0].values.length)
|
|
74
|
+
return;
|
|
75
|
+
for (const row of result[0].values) {
|
|
76
|
+
const address = row[0];
|
|
77
|
+
try {
|
|
78
|
+
const mnemonic = new Entry(KEYCHAIN_SERVICE, address).getPassword();
|
|
79
|
+
if (mnemonic) {
|
|
80
|
+
database.run('UPDATE accounts SET mnemonic = ? WHERE address = ?', [mnemonic, address]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// No keychain entry for this address (or keychain unavailable). Leave
|
|
85
|
+
// mnemonic NULL — the row remains until the user re-imports or replaces it.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
55
89
|
function persistDb() {
|
|
56
90
|
if (!db)
|
|
57
91
|
return;
|
|
58
92
|
const data = db.export();
|
|
59
|
-
writeFileSync(DB_PATH, Buffer.from(data));
|
|
93
|
+
writeFileSync(DB_PATH, Buffer.from(data), { mode: 0o600 });
|
|
94
|
+
try {
|
|
95
|
+
chmodSync(DB_PATH, 0o600);
|
|
96
|
+
}
|
|
97
|
+
catch { /* best-effort on platforms that don't support it */ }
|
|
60
98
|
}
|
|
61
99
|
// ── Tool Schemas ─────────────────────────────────────────────────────────────
|
|
62
100
|
const walletToolSchemas = {
|
|
@@ -184,35 +222,59 @@ export class WalletManager {
|
|
|
184
222
|
}
|
|
185
223
|
throw new McpError(ErrorCode.InvalidParams, 'Provide either nickname or index');
|
|
186
224
|
}
|
|
187
|
-
// ──
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
225
|
+
// ── Mnemonic Storage ───────────────────────────────────────────────────────
|
|
226
|
+
//
|
|
227
|
+
// Mnemonics live in the `accounts.mnemonic` column of wallet.db. The OS
|
|
228
|
+
// keychain (@napi-rs/keyring) is kept only as a backward-compatibility read
|
|
229
|
+
// fallback for accounts that pre-date this change; the eager migration in
|
|
230
|
+
// getDb() copies any such mnemonics into the DB at startup, and the fallback
|
|
231
|
+
// below catches the rare case where the keychain was unavailable at startup
|
|
232
|
+
// but becomes available later. New mnemonics are never written to the
|
|
233
|
+
// keychain.
|
|
234
|
+
static async storeMnemonic(address, mnemonic) {
|
|
235
|
+
const database = await getDb();
|
|
236
|
+
database.run('UPDATE accounts SET mnemonic = ? WHERE address = ?', [mnemonic, address]);
|
|
237
|
+
persistDb();
|
|
191
238
|
}
|
|
192
|
-
static getMnemonic(address) {
|
|
239
|
+
static async getMnemonic(address) {
|
|
240
|
+
const database = await getDb();
|
|
241
|
+
const result = database.exec('SELECT mnemonic FROM accounts WHERE address = ?', [address]);
|
|
242
|
+
if (result.length && result[0].values.length) {
|
|
243
|
+
const m = result[0].values[0][0];
|
|
244
|
+
if (typeof m === 'string' && m.length > 0)
|
|
245
|
+
return m;
|
|
246
|
+
}
|
|
247
|
+
// Fallback: read from keychain (handles the case where eager migration
|
|
248
|
+
// missed this address because the keychain wasn't available at startup).
|
|
249
|
+
// On success, copy into the DB so future reads are DB-native.
|
|
193
250
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
251
|
+
const m = new Entry(KEYCHAIN_SERVICE, address).getPassword();
|
|
252
|
+
if (m) {
|
|
253
|
+
database.run('UPDATE accounts SET mnemonic = ? WHERE address = ?', [m, address]);
|
|
254
|
+
persistDb();
|
|
255
|
+
return m;
|
|
256
|
+
}
|
|
199
257
|
}
|
|
200
258
|
catch {
|
|
201
|
-
|
|
259
|
+
// No keychain entry / keychain unavailable.
|
|
202
260
|
}
|
|
261
|
+
throw new McpError(ErrorCode.InvalidParams, `No mnemonic found for address: ${address}`);
|
|
203
262
|
}
|
|
204
|
-
static deleteMnemonic(address) {
|
|
263
|
+
static async deleteMnemonic(address) {
|
|
264
|
+
const database = await getDb();
|
|
265
|
+
database.run('UPDATE accounts SET mnemonic = NULL WHERE address = ?', [address]);
|
|
266
|
+
persistDb();
|
|
267
|
+
// Best-effort cleanup of any stale keychain entry left over from a
|
|
268
|
+
// pre-migration install. Failure here is harmless.
|
|
205
269
|
try {
|
|
206
|
-
|
|
207
|
-
return entry.deleteCredential();
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return false;
|
|
270
|
+
new Entry(KEYCHAIN_SERVICE, address).deleteCredential();
|
|
211
271
|
}
|
|
272
|
+
catch { /* ignore */ }
|
|
273
|
+
return true;
|
|
212
274
|
}
|
|
213
275
|
static async getActiveSecretKey() {
|
|
214
276
|
const account = await WalletManager.getActiveAccount();
|
|
215
|
-
const mnemonic = WalletManager.getMnemonic(account.address);
|
|
277
|
+
const mnemonic = await WalletManager.getMnemonic(account.address);
|
|
216
278
|
return algosdk.mnemonicToSecretKey(mnemonic).sk;
|
|
217
279
|
}
|
|
218
280
|
// ── Transaction Helpers ────────────────────────────────────────────────────
|
|
@@ -376,9 +438,7 @@ export class WalletManager {
|
|
|
376
438
|
if (addrExists.length > 0 && addrExists[0].values.length > 0) {
|
|
377
439
|
throw new McpError(ErrorCode.InvalidParams, `Account with address ${address} already exists in wallet`);
|
|
378
440
|
}
|
|
379
|
-
|
|
380
|
-
WalletManager.storeMnemonic(address, mnemonic);
|
|
381
|
-
database.run('INSERT INTO accounts (address, public_key, nickname, created_at) VALUES (?, ?, ?, ?)', [address, publicKey, args.nickname, new Date().toISOString()]);
|
|
441
|
+
database.run('INSERT INTO accounts (address, public_key, nickname, mnemonic, created_at) VALUES (?, ?, ?, ?, ?)', [address, publicKey, args.nickname, mnemonic, new Date().toISOString()]);
|
|
382
442
|
persistDb();
|
|
383
443
|
const accounts = await WalletManager.getAllAccounts();
|
|
384
444
|
const newIndex = accounts.findIndex(a => a.address === address);
|
|
@@ -406,7 +466,7 @@ export class WalletManager {
|
|
|
406
466
|
}
|
|
407
467
|
const idx = await WalletManager.resolveAccountIndex(args);
|
|
408
468
|
const removed = accounts[idx];
|
|
409
|
-
WalletManager.deleteMnemonic(removed.address);
|
|
469
|
+
await WalletManager.deleteMnemonic(removed.address);
|
|
410
470
|
const database = await getDb();
|
|
411
471
|
database.run('DELETE FROM accounts WHERE address = ?', [removed.address]);
|
|
412
472
|
// Adjust active index
|
|
@@ -675,7 +735,7 @@ export class WalletManager {
|
|
|
675
735
|
WalletManager.walletTools = [
|
|
676
736
|
{
|
|
677
737
|
name: 'wallet_add_account',
|
|
678
|
-
description: 'Create a new Algorand account
|
|
738
|
+
description: 'Create a new Algorand account, store it in the agent-wallet database with a nickname, and auto-switch to it if it is the first account. Returns address, public key, nickname, and index. The mnemonic is held internally by the MCP server and never returned to the agent.',
|
|
679
739
|
inputSchema: withCommonParams(walletToolSchemas.addAccount),
|
|
680
740
|
},
|
|
681
741
|
{
|