@goplausible/algorand-mcp 4.2.9 → 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.
@@ -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.2.8',
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).',
@@ -3,6 +3,7 @@ export interface AccountRow {
3
3
  address: string;
4
4
  public_key: string;
5
5
  nickname: string;
6
+ mnemonic: string | null;
6
7
  created_at: string;
7
8
  }
8
9
  export declare class WalletManager {
@@ -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
- // ── Keychain Access ────────────────────────────────────────────────────────
188
- static storeMnemonic(address, mnemonic) {
189
- const entry = new Entry(KEYCHAIN_SERVICE, address);
190
- entry.setPassword(mnemonic);
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 entry = new Entry(KEYCHAIN_SERVICE, address);
195
- const mnemonic = entry.getPassword();
196
- if (!mnemonic)
197
- throw new Error('No mnemonic found');
198
- return mnemonic;
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
- throw new McpError(ErrorCode.InvalidParams, `No keychain entry found for address: ${address}`);
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
- const entry = new Entry(KEYCHAIN_SERVICE, address);
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
- // Store mnemonic in OS keychain
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 and store it securely in the OS keychain with a nickname. Returns only address and public key.',
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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplausible/algorand-mcp",
3
- "version": "4.2.9",
3
+ "version": "4.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },