@goplausible/algorand-mcp 4.2.9 → 4.3.1
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 -2
- package/dist/tools/utilityManager.js +1 -1
- package/dist/tools/walletManager.d.ts +10 -0
- package/dist/tools/walletManager.js +200 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -319,7 +319,28 @@ Older installs of this MCP stored mnemonics in the OS keychain (`@napi-rs/keyrin
|
|
|
319
319
|
|
|
320
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.
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
**Orphan handling (archive, not delete).** If an `accounts` row exists but its mnemonic isn't in the keychain *and* isn't already in the DB (e.g., the user copied `wallet.db` to a new machine without also moving the keychain entries, restored from a partial backup, or installed in Docker where the keychain never existed), that row is unusable for signing. Rather than delete it, the server marks the row as **archived** (`UPDATE accounts SET archived = 1 WHERE mnemonic IS NULL OR mnemonic = ''`). Archived rows:
|
|
323
|
+
|
|
324
|
+
- are **hidden from the default** `wallet_list_accounts` response
|
|
325
|
+
- **never become the active account** (the active-account index is clamped to the end of the remaining active list, or reset to `0` if no active accounts remain)
|
|
326
|
+
- keep their **original nickname** (a partial unique index `idx_active_nickname` enforces nickname uniqueness only among active rows, so a new `wallet_add_account` can reuse the same nickname for a fresh keypair)
|
|
327
|
+
- are **surfaced via `wallet_list_accounts { archived: true }`** for forensics or future recovery
|
|
328
|
+
|
|
329
|
+
Archiving is silent at the MCP tool layer. The only diagnostic is a one-line stderr log per failed keychain read (`[algorand-mcp] keychain read failed for <addr>…: <msg>`), so if a user investigates a false archive they can see whether the keychain threw "no entry" vs "access denied" vs "no DBus" etc.
|
|
330
|
+
|
|
331
|
+
No user action is required for any of this. No prompts, no env vars, no migration tools.
|
|
332
|
+
|
|
333
|
+
### Schema versions
|
|
334
|
+
|
|
335
|
+
The DB schema evolves additively via an idempotent migration that runs at startup:
|
|
336
|
+
|
|
337
|
+
| Version | Change |
|
|
338
|
+
|---|---|
|
|
339
|
+
| v1 | initial — `accounts` columns: `id`, `address`, `public_key`, `nickname` (UNIQUE), `created_at` |
|
|
340
|
+
| v2 | added `mnemonic TEXT` column |
|
|
341
|
+
| v3 | added `archived INTEGER NOT NULL DEFAULT 0` column; dropped the column-level UNIQUE on `nickname` and replaced it with `CREATE UNIQUE INDEX idx_active_nickname ON accounts(nickname) WHERE archived = 0` so archived rows can keep their original nicknames without blocking reuse |
|
|
342
|
+
|
|
343
|
+
The v2→v3 step recreates the `accounts` table (SQLite cannot drop a column-level UNIQUE constraint via ALTER) and copies data forward with `archived = 0`. Existing wallets keep working unchanged.
|
|
323
344
|
|
|
324
345
|
## x402 HTTP Payments
|
|
325
346
|
|
|
@@ -451,7 +472,7 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
451
472
|
|---|---|
|
|
452
473
|
| `wallet_add_account` | Create a new Algorand account with nickname (returns address + public key only) |
|
|
453
474
|
| `wallet_remove_account` | Remove an account from the wallet by nickname or index |
|
|
454
|
-
| `wallet_list_accounts` | List
|
|
475
|
+
| `wallet_list_accounts` | List active accounts with nicknames and addresses. Pass `{ archived: true }` to list archived accounts instead (rows whose mnemonic couldn't be recovered from the OS keychain at startup — kept in the DB for forensics, not signable). |
|
|
455
476
|
| `wallet_switch_account` | Switch the active account by nickname or index |
|
|
456
477
|
| `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`. |
|
|
457
478
|
| `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`. |
|
|
@@ -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.1',
|
|
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).',
|
|
@@ -3,6 +3,8 @@ export interface AccountRow {
|
|
|
3
3
|
address: string;
|
|
4
4
|
public_key: string;
|
|
5
5
|
nickname: string;
|
|
6
|
+
mnemonic: string | null;
|
|
7
|
+
archived: number;
|
|
6
8
|
created_at: string;
|
|
7
9
|
}
|
|
8
10
|
export declare class WalletManager {
|
|
@@ -11,6 +13,14 @@ export declare class WalletManager {
|
|
|
11
13
|
description: string;
|
|
12
14
|
inputSchema: any;
|
|
13
15
|
}[];
|
|
16
|
+
/**
|
|
17
|
+
* Returns accounts from wallet.db.
|
|
18
|
+
* - includeArchived = false (default): only active rows (archived = 0).
|
|
19
|
+
* Used by every signing/lookup path and by wallet_list_accounts in its
|
|
20
|
+
* default mode.
|
|
21
|
+
* - includeArchived = true: only archived rows (archived = 1). Used by
|
|
22
|
+
* wallet_list_accounts { archived: true } for forensics.
|
|
23
|
+
*/
|
|
14
24
|
private static getAllAccounts;
|
|
15
25
|
private static getActiveIndex;
|
|
16
26
|
private static setActiveIndex;
|
|
@@ -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 ────────────────────────────────────────────────────────────────
|
|
@@ -28,13 +28,16 @@ async function getDb() {
|
|
|
28
28
|
else {
|
|
29
29
|
db = new SQL.Database();
|
|
30
30
|
}
|
|
31
|
-
// Create tables if they don't exist
|
|
31
|
+
// Create tables if they don't exist. For fresh installs the schema is
|
|
32
|
+
// already at the latest shape; for older DBs ensureSchema() below upgrades.
|
|
32
33
|
db.run(`
|
|
33
34
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
34
35
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
36
|
address TEXT UNIQUE NOT NULL,
|
|
36
37
|
public_key TEXT NOT NULL,
|
|
37
|
-
nickname TEXT
|
|
38
|
+
nickname TEXT NOT NULL,
|
|
39
|
+
mnemonic TEXT,
|
|
40
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
38
41
|
created_at TEXT NOT NULL
|
|
39
42
|
)
|
|
40
43
|
`);
|
|
@@ -44,19 +47,137 @@ async function getDb() {
|
|
|
44
47
|
value TEXT NOT NULL
|
|
45
48
|
)
|
|
46
49
|
`);
|
|
50
|
+
// Idempotent schema migration: add mnemonic + archived columns on
|
|
51
|
+
// pre-existing DBs; replace the legacy column-level UNIQUE on nickname with
|
|
52
|
+
// a partial unique index that only enforces uniqueness among non-archived
|
|
53
|
+
// accounts (so an archived account can keep its original nickname while a
|
|
54
|
+
// new active account with the same nickname can be created).
|
|
55
|
+
ensureSchema(db);
|
|
47
56
|
// Initialize active index if not set
|
|
48
57
|
const row = db.exec("SELECT value FROM wallet_state WHERE key = 'active_account_index'");
|
|
49
58
|
if (row.length === 0) {
|
|
50
59
|
db.run("INSERT INTO wallet_state (key, value) VALUES ('active_account_index', '0')");
|
|
51
60
|
}
|
|
61
|
+
// Eager keychain → DB migration for any pre-existing accounts whose mnemonic
|
|
62
|
+
// still lives in the OS keychain. Runs once per startup; idempotent (rows
|
|
63
|
+
// already populated are skipped).
|
|
64
|
+
migrateKeychainToDb(db);
|
|
65
|
+
// Any row that's still NULL after the migration attempt is an orphan
|
|
66
|
+
// (e.g. wallet.db was restored without its keychain counterpart, or this
|
|
67
|
+
// is a Docker install where the keychain never existed). Archive them so
|
|
68
|
+
// the agent doesn't see unusable accounts in the default wallet_list, but
|
|
69
|
+
// the rows stay in the DB and can be surfaced via wallet_list_accounts
|
|
70
|
+
// { archived: true } for forensics or future recovery.
|
|
71
|
+
archiveOrphanedAccounts(db);
|
|
52
72
|
persistDb();
|
|
53
73
|
return db;
|
|
54
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Brings older DBs forward to the latest schema:
|
|
77
|
+
* v1 → no mnemonic column, nickname is a column-level UNIQUE
|
|
78
|
+
* v2 → mnemonic column added, nickname still column-level UNIQUE
|
|
79
|
+
* v3 → archived column added, nickname UNIQUE replaced with a partial unique
|
|
80
|
+
* index (idx_active_nickname) that only enforces uniqueness among rows
|
|
81
|
+
* WHERE archived = 0
|
|
82
|
+
*
|
|
83
|
+
* SQLite cannot drop a column-level UNIQUE constraint via ALTER, so when the
|
|
84
|
+
* upgrade to v3 is needed we recreate the table. Idempotent: subsequent
|
|
85
|
+
* startups detect the archived column already exists and skip.
|
|
86
|
+
*/
|
|
87
|
+
function ensureSchema(database) {
|
|
88
|
+
const info = database.exec('PRAGMA table_info(accounts)');
|
|
89
|
+
if (!info.length)
|
|
90
|
+
return;
|
|
91
|
+
const cols = info[0].values.map(r => r[1]);
|
|
92
|
+
// v1 → v2: add mnemonic column
|
|
93
|
+
if (!cols.includes('mnemonic')) {
|
|
94
|
+
database.run('ALTER TABLE accounts ADD COLUMN mnemonic TEXT');
|
|
95
|
+
cols.push('mnemonic');
|
|
96
|
+
}
|
|
97
|
+
// v2 → v3: add archived column + drop nickname UNIQUE via table recreation
|
|
98
|
+
if (!cols.includes('archived')) {
|
|
99
|
+
database.run(`
|
|
100
|
+
CREATE TABLE accounts_new (
|
|
101
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
102
|
+
address TEXT UNIQUE NOT NULL,
|
|
103
|
+
public_key TEXT NOT NULL,
|
|
104
|
+
nickname TEXT NOT NULL,
|
|
105
|
+
mnemonic TEXT,
|
|
106
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
created_at TEXT NOT NULL
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
database.run(`
|
|
111
|
+
INSERT INTO accounts_new (id, address, public_key, nickname, mnemonic, archived, created_at)
|
|
112
|
+
SELECT id, address, public_key, nickname, mnemonic, 0, created_at FROM accounts
|
|
113
|
+
`);
|
|
114
|
+
database.run('DROP TABLE accounts');
|
|
115
|
+
database.run('ALTER TABLE accounts_new RENAME TO accounts');
|
|
116
|
+
}
|
|
117
|
+
// Partial unique index — applies to fresh and migrated DBs. Idempotent.
|
|
118
|
+
database.run('CREATE UNIQUE INDEX IF NOT EXISTS idx_active_nickname ON accounts(nickname) WHERE archived = 0');
|
|
119
|
+
}
|
|
120
|
+
function migrateKeychainToDb(database) {
|
|
121
|
+
const result = database.exec("SELECT address FROM accounts WHERE (mnemonic IS NULL OR mnemonic = '') AND archived = 0");
|
|
122
|
+
if (!result.length || !result[0].values.length)
|
|
123
|
+
return;
|
|
124
|
+
for (const row of result[0].values) {
|
|
125
|
+
const address = row[0];
|
|
126
|
+
let mnemonic = null;
|
|
127
|
+
try {
|
|
128
|
+
mnemonic = new Entry(KEYCHAIN_SERVICE, address).getPassword();
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// Keychain unavailable, entry not found, or permission denied. Log to
|
|
132
|
+
// stderr so a user investigating a false archive can see what happened.
|
|
133
|
+
// The MCP tool surface remains silent.
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
console.error(`[algorand-mcp] keychain read failed for ${address.slice(0, 10)}…: ${msg}`);
|
|
136
|
+
}
|
|
137
|
+
if (mnemonic && mnemonic.length > 0) {
|
|
138
|
+
database.run('UPDATE accounts SET mnemonic = ? WHERE address = ?', [mnemonic, address]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Marks any `accounts` rows that still have no mnemonic after the keychain
|
|
144
|
+
* migration step as archived (archived = 1). Archived rows:
|
|
145
|
+
* - are hidden from the default wallet_list_accounts response
|
|
146
|
+
* - never become the active account
|
|
147
|
+
* - keep their original nickname (the partial unique index on
|
|
148
|
+
* idx_active_nickname allows a new active account to reuse the nickname)
|
|
149
|
+
* - are surfaced via wallet_list_accounts { archived: true } for forensics
|
|
150
|
+
*
|
|
151
|
+
* Silent at the MCP tool layer. The active-account index is clamped to the
|
|
152
|
+
* end of the remaining active list (or 0 if no active accounts remain).
|
|
153
|
+
*/
|
|
154
|
+
function archiveOrphanedAccounts(database) {
|
|
155
|
+
const orphans = database.exec("SELECT id FROM accounts WHERE (mnemonic IS NULL OR mnemonic = '') AND archived = 0");
|
|
156
|
+
if (!orphans.length || !orphans[0].values.length)
|
|
157
|
+
return;
|
|
158
|
+
database.run("UPDATE accounts SET archived = 1 WHERE (mnemonic IS NULL OR mnemonic = '') AND archived = 0");
|
|
159
|
+
// Clamp active_account_index against the (now reduced) active list.
|
|
160
|
+
const countResult = database.exec('SELECT COUNT(*) FROM accounts WHERE archived = 0');
|
|
161
|
+
const remaining = countResult.length && countResult[0].values.length
|
|
162
|
+
? Number(countResult[0].values[0][0])
|
|
163
|
+
: 0;
|
|
164
|
+
const activeResult = database.exec("SELECT value FROM wallet_state WHERE key = 'active_account_index'");
|
|
165
|
+
const activeIdx = activeResult.length && activeResult[0].values.length
|
|
166
|
+
? parseInt(activeResult[0].values[0][0], 10) || 0
|
|
167
|
+
: 0;
|
|
168
|
+
if (remaining === 0 || activeIdx >= remaining) {
|
|
169
|
+
database.run("UPDATE wallet_state SET value = '0' WHERE key = 'active_account_index'");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
55
172
|
function persistDb() {
|
|
56
173
|
if (!db)
|
|
57
174
|
return;
|
|
58
175
|
const data = db.export();
|
|
59
|
-
writeFileSync(DB_PATH, Buffer.from(data));
|
|
176
|
+
writeFileSync(DB_PATH, Buffer.from(data), { mode: 0o600 });
|
|
177
|
+
try {
|
|
178
|
+
chmodSync(DB_PATH, 0o600);
|
|
179
|
+
}
|
|
180
|
+
catch { /* best-effort on platforms that don't support it */ }
|
|
60
181
|
}
|
|
61
182
|
// ── Tool Schemas ─────────────────────────────────────────────────────────────
|
|
62
183
|
const walletToolSchemas = {
|
|
@@ -77,7 +198,12 @@ const walletToolSchemas = {
|
|
|
77
198
|
},
|
|
78
199
|
listAccounts: {
|
|
79
200
|
type: 'object',
|
|
80
|
-
properties: {
|
|
201
|
+
properties: {
|
|
202
|
+
archived: {
|
|
203
|
+
type: 'boolean',
|
|
204
|
+
description: 'If true, return archived accounts (rows whose mnemonic could not be recovered from the OS keychain at startup, kept in the DB for forensics). If false or omitted, returns only active (signable) accounts.'
|
|
205
|
+
}
|
|
206
|
+
},
|
|
81
207
|
required: []
|
|
82
208
|
},
|
|
83
209
|
switchAccount: {
|
|
@@ -134,9 +260,20 @@ const walletToolSchemas = {
|
|
|
134
260
|
// ── Tool Definitions ─────────────────────────────────────────────────────────
|
|
135
261
|
export class WalletManager {
|
|
136
262
|
// ── Database Helpers ───────────────────────────────────────────────────────
|
|
137
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Returns accounts from wallet.db.
|
|
265
|
+
* - includeArchived = false (default): only active rows (archived = 0).
|
|
266
|
+
* Used by every signing/lookup path and by wallet_list_accounts in its
|
|
267
|
+
* default mode.
|
|
268
|
+
* - includeArchived = true: only archived rows (archived = 1). Used by
|
|
269
|
+
* wallet_list_accounts { archived: true } for forensics.
|
|
270
|
+
*/
|
|
271
|
+
static async getAllAccounts(includeArchived = false) {
|
|
138
272
|
const database = await getDb();
|
|
139
|
-
const
|
|
273
|
+
const sql = includeArchived
|
|
274
|
+
? 'SELECT * FROM accounts WHERE archived = 1 ORDER BY id'
|
|
275
|
+
: 'SELECT * FROM accounts WHERE archived = 0 ORDER BY id';
|
|
276
|
+
const result = database.exec(sql);
|
|
140
277
|
if (result.length === 0)
|
|
141
278
|
return [];
|
|
142
279
|
const cols = result[0].columns;
|
|
@@ -184,35 +321,59 @@ export class WalletManager {
|
|
|
184
321
|
}
|
|
185
322
|
throw new McpError(ErrorCode.InvalidParams, 'Provide either nickname or index');
|
|
186
323
|
}
|
|
187
|
-
// ──
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
324
|
+
// ── Mnemonic Storage ───────────────────────────────────────────────────────
|
|
325
|
+
//
|
|
326
|
+
// Mnemonics live in the `accounts.mnemonic` column of wallet.db. The OS
|
|
327
|
+
// keychain (@napi-rs/keyring) is kept only as a backward-compatibility read
|
|
328
|
+
// fallback for accounts that pre-date this change; the eager migration in
|
|
329
|
+
// getDb() copies any such mnemonics into the DB at startup, and the fallback
|
|
330
|
+
// below catches the rare case where the keychain was unavailable at startup
|
|
331
|
+
// but becomes available later. New mnemonics are never written to the
|
|
332
|
+
// keychain.
|
|
333
|
+
static async storeMnemonic(address, mnemonic) {
|
|
334
|
+
const database = await getDb();
|
|
335
|
+
database.run('UPDATE accounts SET mnemonic = ? WHERE address = ?', [mnemonic, address]);
|
|
336
|
+
persistDb();
|
|
191
337
|
}
|
|
192
|
-
static getMnemonic(address) {
|
|
338
|
+
static async getMnemonic(address) {
|
|
339
|
+
const database = await getDb();
|
|
340
|
+
const result = database.exec('SELECT mnemonic FROM accounts WHERE address = ?', [address]);
|
|
341
|
+
if (result.length && result[0].values.length) {
|
|
342
|
+
const m = result[0].values[0][0];
|
|
343
|
+
if (typeof m === 'string' && m.length > 0)
|
|
344
|
+
return m;
|
|
345
|
+
}
|
|
346
|
+
// Fallback: read from keychain (handles the case where eager migration
|
|
347
|
+
// missed this address because the keychain wasn't available at startup).
|
|
348
|
+
// On success, copy into the DB so future reads are DB-native.
|
|
193
349
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
350
|
+
const m = new Entry(KEYCHAIN_SERVICE, address).getPassword();
|
|
351
|
+
if (m) {
|
|
352
|
+
database.run('UPDATE accounts SET mnemonic = ? WHERE address = ?', [m, address]);
|
|
353
|
+
persistDb();
|
|
354
|
+
return m;
|
|
355
|
+
}
|
|
199
356
|
}
|
|
200
357
|
catch {
|
|
201
|
-
|
|
358
|
+
// No keychain entry / keychain unavailable.
|
|
202
359
|
}
|
|
360
|
+
throw new McpError(ErrorCode.InvalidParams, `No mnemonic found for address: ${address}`);
|
|
203
361
|
}
|
|
204
|
-
static deleteMnemonic(address) {
|
|
362
|
+
static async deleteMnemonic(address) {
|
|
363
|
+
const database = await getDb();
|
|
364
|
+
database.run('UPDATE accounts SET mnemonic = NULL WHERE address = ?', [address]);
|
|
365
|
+
persistDb();
|
|
366
|
+
// Best-effort cleanup of any stale keychain entry left over from a
|
|
367
|
+
// pre-migration install. Failure here is harmless.
|
|
205
368
|
try {
|
|
206
|
-
|
|
207
|
-
return entry.deleteCredential();
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return false;
|
|
369
|
+
new Entry(KEYCHAIN_SERVICE, address).deleteCredential();
|
|
211
370
|
}
|
|
371
|
+
catch { /* ignore */ }
|
|
372
|
+
return true;
|
|
212
373
|
}
|
|
213
374
|
static async getActiveSecretKey() {
|
|
214
375
|
const account = await WalletManager.getActiveAccount();
|
|
215
|
-
const mnemonic = WalletManager.getMnemonic(account.address);
|
|
376
|
+
const mnemonic = await WalletManager.getMnemonic(account.address);
|
|
216
377
|
return algosdk.mnemonicToSecretKey(mnemonic).sk;
|
|
217
378
|
}
|
|
218
379
|
// ── Transaction Helpers ────────────────────────────────────────────────────
|
|
@@ -360,7 +521,10 @@ export class WalletManager {
|
|
|
360
521
|
}
|
|
361
522
|
const database = await getDb();
|
|
362
523
|
// Check nickname uniqueness
|
|
363
|
-
|
|
524
|
+
// Nickname uniqueness is enforced only among ACTIVE accounts (the
|
|
525
|
+
// partial unique index idx_active_nickname). Archived rows with the
|
|
526
|
+
// same nickname don't conflict — the new account takes over the name.
|
|
527
|
+
const existing = database.exec('SELECT id FROM accounts WHERE nickname = ? AND archived = 0', [args.nickname]);
|
|
364
528
|
if (existing.length > 0 && existing[0].values.length > 0) {
|
|
365
529
|
throw new McpError(ErrorCode.InvalidParams, `Account with nickname "${args.nickname}" already exists`);
|
|
366
530
|
}
|
|
@@ -376,9 +540,7 @@ export class WalletManager {
|
|
|
376
540
|
if (addrExists.length > 0 && addrExists[0].values.length > 0) {
|
|
377
541
|
throw new McpError(ErrorCode.InvalidParams, `Account with address ${address} already exists in wallet`);
|
|
378
542
|
}
|
|
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()]);
|
|
543
|
+
database.run('INSERT INTO accounts (address, public_key, nickname, mnemonic, created_at) VALUES (?, ?, ?, ?, ?)', [address, publicKey, args.nickname, mnemonic, new Date().toISOString()]);
|
|
382
544
|
persistDb();
|
|
383
545
|
const accounts = await WalletManager.getAllAccounts();
|
|
384
546
|
const newIndex = accounts.findIndex(a => a.address === address);
|
|
@@ -406,7 +568,7 @@ export class WalletManager {
|
|
|
406
568
|
}
|
|
407
569
|
const idx = await WalletManager.resolveAccountIndex(args);
|
|
408
570
|
const removed = accounts[idx];
|
|
409
|
-
WalletManager.deleteMnemonic(removed.address);
|
|
571
|
+
await WalletManager.deleteMnemonic(removed.address);
|
|
410
572
|
const database = await getDb();
|
|
411
573
|
database.run('DELETE FROM accounts WHERE address = ?', [removed.address]);
|
|
412
574
|
// Adjust active index
|
|
@@ -432,17 +594,20 @@ export class WalletManager {
|
|
|
432
594
|
}
|
|
433
595
|
// ── wallet_list_accounts ─────────────────────────────────────────
|
|
434
596
|
case 'wallet_list_accounts': {
|
|
435
|
-
const
|
|
436
|
-
const
|
|
597
|
+
const showArchived = args.archived === true;
|
|
598
|
+
const accounts = await WalletManager.getAllAccounts(showArchived);
|
|
599
|
+
const activeIdx = showArchived ? -1 : await WalletManager.getActiveIndex();
|
|
437
600
|
return {
|
|
438
601
|
content: [{
|
|
439
602
|
type: 'text',
|
|
440
603
|
text: JSON.stringify({
|
|
604
|
+
archived: showArchived,
|
|
441
605
|
activeIndex: activeIdx,
|
|
442
606
|
count: accounts.length,
|
|
443
607
|
accounts: accounts.map((a, i) => ({
|
|
444
608
|
index: i,
|
|
445
|
-
active: i === activeIdx,
|
|
609
|
+
active: !showArchived && i === activeIdx,
|
|
610
|
+
archived: showArchived,
|
|
446
611
|
nickname: a.nickname,
|
|
447
612
|
address: a.address,
|
|
448
613
|
publicKey: a.public_key,
|
|
@@ -675,7 +840,7 @@ export class WalletManager {
|
|
|
675
840
|
WalletManager.walletTools = [
|
|
676
841
|
{
|
|
677
842
|
name: 'wallet_add_account',
|
|
678
|
-
description: 'Create a new Algorand account
|
|
843
|
+
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
844
|
inputSchema: withCommonParams(walletToolSchemas.addAccount),
|
|
680
845
|
},
|
|
681
846
|
{
|
|
@@ -685,7 +850,7 @@ WalletManager.walletTools = [
|
|
|
685
850
|
},
|
|
686
851
|
{
|
|
687
852
|
name: 'wallet_list_accounts',
|
|
688
|
-
description: 'List
|
|
853
|
+
description: 'List wallet accounts with their nicknames and addresses. By default returns only ACTIVE accounts (signable). Pass { archived: true } to return ARCHIVED accounts instead — these are accounts whose mnemonic could not be recovered from the OS keychain at startup (e.g., wallet.db moved to a new machine without keychain, or fresh Docker install over an existing DB). Archived rows stay in the DB for forensics but cannot sign; their nicknames are freed for reuse by new active accounts.',
|
|
689
854
|
inputSchema: withCommonParams(walletToolSchemas.listAccounts),
|
|
690
855
|
},
|
|
691
856
|
{
|