@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 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
- No user action is required. No prompts, no env vars, no migration logs.
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 all accounts with nicknames and addresses |
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.2.8',
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 UNIQUE NOT NULL,
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
- static async getAllAccounts() {
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 result = database.exec('SELECT * FROM accounts ORDER BY id');
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
- // ── Keychain Access ────────────────────────────────────────────────────────
188
- static storeMnemonic(address, mnemonic) {
189
- const entry = new Entry(KEYCHAIN_SERVICE, address);
190
- entry.setPassword(mnemonic);
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 entry = new Entry(KEYCHAIN_SERVICE, address);
195
- const mnemonic = entry.getPassword();
196
- if (!mnemonic)
197
- throw new Error('No mnemonic found');
198
- return mnemonic;
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
- throw new McpError(ErrorCode.InvalidParams, `No keychain entry found for address: ${address}`);
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
- const entry = new Entry(KEYCHAIN_SERVICE, address);
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
- const existing = database.exec('SELECT id FROM accounts WHERE nickname = ?', [args.nickname]);
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
- // 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()]);
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 accounts = await WalletManager.getAllAccounts();
436
- const activeIdx = await WalletManager.getActiveIndex();
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 and store it securely in the OS keychain with a nickname. Returns only address and public key.',
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 all wallet accounts with their nicknames and addresses.',
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
  {
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.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },