@goplausible/algorand-mcp 4.2.4 → 4.2.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 +10 -18
- package/dist/networkConfig.js +3 -3
- package/dist/tools/apiManager/hayrouter/swap.js +3 -8
- package/dist/tools/apiManager/indexer/account.js +5 -5
- package/dist/tools/apiManager/indexer/application.js +5 -5
- package/dist/tools/apiManager/indexer/asset.js +4 -4
- package/dist/tools/apiManager/indexer/transaction.js +3 -3
- package/dist/tools/commonParams.js +1 -1
- package/dist/tools/utilityManager.js +1 -1
- package/dist/tools/walletManager.d.ts +1 -12
- package/dist/tools/walletManager.js +8 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
16
|
- Secure wallet management via OS keychain — private keys never exposed to agents or LLMs
|
|
17
|
-
- Wallet accounts
|
|
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)
|
|
20
20
|
- Atomic transaction groups
|
|
@@ -276,7 +276,7 @@ The wallet system has two layers of storage, each with a distinct security role:
|
|
|
276
276
|
| Layer | What it stores | Where | Encryption |
|
|
277
277
|
|---|---|---|---|
|
|
278
278
|
| **OS Keychain** | Mnemonics (secret keys) | macOS Keychain / Linux libsecret / Windows Credential Manager | OS-managed, hardware-backed where available |
|
|
279
|
-
| **Embedded SQLite** | Account metadata (nicknames,
|
|
279
|
+
| **Embedded SQLite** | Account metadata (nicknames, active-account index) | `~/.algorand-mcp/wallet.db` | Plaintext (no secrets) |
|
|
280
280
|
|
|
281
281
|
**Private key material never appears in tool responses, MCP config files, environment variables, or logs.** The agent only sees addresses, public keys, and signed transaction blobs.
|
|
282
282
|
|
|
@@ -290,31 +290,23 @@ The wallet system has two layers of storage, each with a distinct security role:
|
|
|
290
290
|
│ { nickname: "main" } │ │
|
|
291
291
|
│ ──────────────────────────► │ generate keypair │
|
|
292
292
|
│ │ store mnemonic ──────────► │ OS Keychain (encrypted)
|
|
293
|
-
│ │ store metadata ──────────► │ SQLite (nickname
|
|
293
|
+
│ │ store metadata ──────────► │ SQLite (nickname)
|
|
294
294
|
│ ◄─ { address, publicKey } │ │
|
|
295
295
|
│ │ │
|
|
296
296
|
│ wallet_sign_transaction │ │
|
|
297
297
|
│ { transaction: {...} } │ │
|
|
298
|
-
│ ──────────────────────────► │
|
|
299
|
-
│ │ retrieve mnemonic ◄────── │ OS Keychain
|
|
298
|
+
│ ──────────────────────────► │ retrieve mnemonic ◄────── │ OS Keychain
|
|
300
299
|
│ │ sign in memory │
|
|
301
300
|
│ ◄─ { txID, blob } │ (key discarded) │
|
|
302
301
|
│ │ │
|
|
303
302
|
```
|
|
304
303
|
|
|
305
|
-
1. **Account creation** (`wallet_add_account`) — Generates a keypair, stores the mnemonic in the OS keychain, and stores
|
|
304
|
+
1. **Account creation** (`wallet_add_account`) — Generates a keypair, stores the mnemonic in the OS keychain, and stores the nickname in SQLite. Returns **only** address and public key.
|
|
306
305
|
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`) —
|
|
306
|
+
3. **Transaction signing** (`wallet_sign_transaction`) — Retrieves the key from the keychain, signs in memory, discards the key. Returns only the signed blob.
|
|
308
307
|
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
308
|
5. **Asset opt-in** (`wallet_optin_asset`) — Creates, signs, and submits an opt-in transaction for the active account in one step.
|
|
310
309
|
|
|
311
|
-
### Spending limits
|
|
312
|
-
|
|
313
|
-
Each account has two configurable limits (in microAlgos, 0 = unlimited):
|
|
314
|
-
|
|
315
|
-
- **`allowance`** — Maximum amount per single transaction. Rejects any transaction exceeding this.
|
|
316
|
-
- **`dailyAllowance`** — Maximum total spend per calendar day across all transactions. Automatically resets at midnight. Tracked in SQLite.
|
|
317
|
-
|
|
318
310
|
### Platform keychain support
|
|
319
311
|
|
|
320
312
|
The keychain backend is provided by [`@napi-rs/keyring`](https://github.com/nicolo-ribaudo/napi-keyring) (Rust-based, prebuilt binaries):
|
|
@@ -365,13 +357,13 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
365
357
|
|
|
366
358
|
| Tool | Description |
|
|
367
359
|
|---|---|
|
|
368
|
-
| `wallet_add_account` | Create a new Algorand account with nickname
|
|
360
|
+
| `wallet_add_account` | Create a new Algorand account with nickname (returns address + public key only) |
|
|
369
361
|
| `wallet_remove_account` | Remove an account from the wallet by nickname or index |
|
|
370
|
-
| `wallet_list_accounts` | List all accounts with nicknames
|
|
362
|
+
| `wallet_list_accounts` | List all accounts with nicknames and addresses |
|
|
371
363
|
| `wallet_switch_account` | Switch the active account by nickname or index |
|
|
372
|
-
| `wallet_get_info` | Get info for the **active account this MCP server owns** (keychain-backed): address, public key, balance, opted-in counts
|
|
364
|
+
| `wallet_get_info` | Get info for the **active account this MCP server owns** (keychain-backed): address, public key, balance, opted-in counts. For arbitrary on-chain accounts use `api_algod_get_account_info`. |
|
|
373
365
|
| `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`. |
|
|
374
|
-
| `wallet_sign_transaction` | Sign a single transaction with the active account
|
|
366
|
+
| `wallet_sign_transaction` | Sign a single transaction with the active account |
|
|
375
367
|
| `wallet_sign_transaction_group` | Sign a group of transactions with the active account (auto-assigns group ID) |
|
|
376
368
|
| `wallet_sign_data` | Sign arbitrary hex data with raw Ed25519 (noble, no SDK prefix) |
|
|
377
369
|
| `wallet_optin_asset` | Opt the active account into an asset (creates, signs, and submits) |
|
package/dist/networkConfig.js
CHANGED
|
@@ -28,7 +28,7 @@ function getEndpoints(network) {
|
|
|
28
28
|
// Memoization caches
|
|
29
29
|
const algodClients = new Map();
|
|
30
30
|
const indexerClients = new Map();
|
|
31
|
-
export function getAlgodClient(network = '
|
|
31
|
+
export function getAlgodClient(network = 'testnet') {
|
|
32
32
|
const key = `${network}:${ALGORAND_TOKEN}`;
|
|
33
33
|
let client = algodClients.get(key);
|
|
34
34
|
if (!client) {
|
|
@@ -38,7 +38,7 @@ export function getAlgodClient(network = 'mainnet') {
|
|
|
38
38
|
}
|
|
39
39
|
return client;
|
|
40
40
|
}
|
|
41
|
-
export function getIndexerClient(network = '
|
|
41
|
+
export function getIndexerClient(network = 'testnet') {
|
|
42
42
|
const key = `${network}:${ALGORAND_TOKEN}`;
|
|
43
43
|
let client = indexerClients.get(key);
|
|
44
44
|
if (!client) {
|
|
@@ -53,5 +53,5 @@ export function extractNetwork(args) {
|
|
|
53
53
|
if (network && !['mainnet', 'testnet', 'localnet'].includes(network)) {
|
|
54
54
|
throw new Error(`Invalid network: ${network}. Must be mainnet, testnet, or localnet.`);
|
|
55
55
|
}
|
|
56
|
-
return (network || '
|
|
56
|
+
return (network || 'testnet');
|
|
57
57
|
}
|
|
@@ -83,10 +83,7 @@ export async function handleSwapTools(args) {
|
|
|
83
83
|
if (maxDepth !== undefined)
|
|
84
84
|
quoteParams.maxDepth = maxDepth;
|
|
85
85
|
const quote = await router.newQuote(quoteParams);
|
|
86
|
-
// 4.
|
|
87
|
-
const estimatedSpend = fromASAID === 0 ? Number(amount) : 0;
|
|
88
|
-
WalletManager.checkWalletSpendingLimits(account, estimatedSpend);
|
|
89
|
-
// 5. Build and execute swap
|
|
86
|
+
// 4. Build and execute swap
|
|
90
87
|
const swapConfig = {
|
|
91
88
|
quote,
|
|
92
89
|
address,
|
|
@@ -98,12 +95,10 @@ export async function handleSwapTools(args) {
|
|
|
98
95
|
}
|
|
99
96
|
const swap = await router.newSwap(swapConfig);
|
|
100
97
|
const result = await swap.execute();
|
|
101
|
-
//
|
|
102
|
-
await WalletManager.recordWalletSpend(address, estimatedSpend);
|
|
103
|
-
// 7. Get swap summary
|
|
98
|
+
// 5. Get swap summary
|
|
104
99
|
const summary = swap.getSummary();
|
|
105
100
|
const inputTxnId = swap.getInputTransactionId();
|
|
106
|
-
//
|
|
101
|
+
// 6. Build response
|
|
107
102
|
const response = {
|
|
108
103
|
status: 'confirmed',
|
|
109
104
|
confirmedRound: result.confirmedRound.toString(),
|
|
@@ -109,7 +109,7 @@ export const accountTools = [
|
|
|
109
109
|
})
|
|
110
110
|
}
|
|
111
111
|
];
|
|
112
|
-
export async function lookupAccountByID(address, network = '
|
|
112
|
+
export async function lookupAccountByID(address, network = 'testnet') {
|
|
113
113
|
try {
|
|
114
114
|
const indexerClient = getIndexerClient(network);
|
|
115
115
|
console.error(`Looking up account info for address ${address}`);
|
|
@@ -125,7 +125,7 @@ export async function lookupAccountByID(address, network = 'mainnet') {
|
|
|
125
125
|
throw new McpError(ErrorCode.InternalError, `Failed to get account info: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
export async function lookupAccountAssets(address, params, network = '
|
|
128
|
+
export async function lookupAccountAssets(address, params, network = 'testnet') {
|
|
129
129
|
try {
|
|
130
130
|
const indexerClient = getIndexerClient(network);
|
|
131
131
|
console.error(`Looking up assets for address ${address}`);
|
|
@@ -151,7 +151,7 @@ export async function lookupAccountAssets(address, params, network = 'mainnet')
|
|
|
151
151
|
throw new McpError(ErrorCode.InternalError, `Failed to get account assets: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
export async function lookupAccountAppLocalStates(address, network = '
|
|
154
|
+
export async function lookupAccountAppLocalStates(address, network = 'testnet') {
|
|
155
155
|
try {
|
|
156
156
|
const indexerClient = getIndexerClient(network);
|
|
157
157
|
console.error(`Looking up app local states for address ${address}`);
|
|
@@ -167,7 +167,7 @@ export async function lookupAccountAppLocalStates(address, network = 'mainnet')
|
|
|
167
167
|
throw new McpError(ErrorCode.InternalError, `Failed to get account application local states: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
export async function lookupAccountCreatedApplications(address, network = '
|
|
170
|
+
export async function lookupAccountCreatedApplications(address, network = 'testnet') {
|
|
171
171
|
try {
|
|
172
172
|
const indexerClient = getIndexerClient(network);
|
|
173
173
|
console.error(`Looking up created applications for address ${address}`);
|
|
@@ -183,7 +183,7 @@ export async function lookupAccountCreatedApplications(address, network = 'mainn
|
|
|
183
183
|
throw new McpError(ErrorCode.InternalError, `Failed to get account created applications: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
export async function searchAccounts(params, network = '
|
|
186
|
+
export async function searchAccounts(params, network = 'testnet') {
|
|
187
187
|
try {
|
|
188
188
|
const indexerClient = getIndexerClient(network);
|
|
189
189
|
console.error('Searching accounts with params:', params);
|
|
@@ -118,7 +118,7 @@ export const applicationTools = [
|
|
|
118
118
|
// })
|
|
119
119
|
// }
|
|
120
120
|
];
|
|
121
|
-
export async function lookupApplications(appId, network = '
|
|
121
|
+
export async function lookupApplications(appId, network = 'testnet') {
|
|
122
122
|
try {
|
|
123
123
|
const indexerClient = getIndexerClient(network);
|
|
124
124
|
console.error(`Looking up application info for ID ${appId}`);
|
|
@@ -134,7 +134,7 @@ export async function lookupApplications(appId, network = 'mainnet') {
|
|
|
134
134
|
throw new McpError(ErrorCode.InternalError, `Failed to get application info: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
export async function lookupApplicationLogs(appId, params, network = '
|
|
137
|
+
export async function lookupApplicationLogs(appId, params, network = 'testnet') {
|
|
138
138
|
try {
|
|
139
139
|
const indexerClient = getIndexerClient(network);
|
|
140
140
|
console.error(`Looking up logs for application ${appId}`);
|
|
@@ -169,7 +169,7 @@ export async function lookupApplicationLogs(appId, params, network = 'mainnet')
|
|
|
169
169
|
throw new McpError(ErrorCode.InternalError, `Failed to get application logs: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
export async function searchForApplications(params, network = '
|
|
172
|
+
export async function searchForApplications(params, network = 'testnet') {
|
|
173
173
|
try {
|
|
174
174
|
const indexerClient = getIndexerClient(network);
|
|
175
175
|
console.error('Searching applications with params:', params);
|
|
@@ -195,7 +195,7 @@ export async function searchForApplications(params, network = 'mainnet') {
|
|
|
195
195
|
throw new McpError(ErrorCode.InternalError, `Failed to search applications: ${error instanceof Error ? error.message : String(error)}`);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
-
export async function lookupApplicationBoxByIDandName(appId, boxName, network = '
|
|
198
|
+
export async function lookupApplicationBoxByIDandName(appId, boxName, network = 'testnet') {
|
|
199
199
|
try {
|
|
200
200
|
const indexerClient = getIndexerClient(network);
|
|
201
201
|
const encoder = new TextEncoder();
|
|
@@ -240,7 +240,7 @@ export async function lookupApplicationBoxByIDandName(appId, boxName, network =
|
|
|
240
240
|
throw new McpError(ErrorCode.InternalError, `Failed to get application box: ${error instanceof Error ? error.message : String(error)}`);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
export async function searchForApplicationBoxes(appId, maxBoxes, network = '
|
|
243
|
+
export async function searchForApplicationBoxes(appId, maxBoxes, network = 'testnet') {
|
|
244
244
|
try {
|
|
245
245
|
const indexerClient = getIndexerClient(network);
|
|
246
246
|
console.error(`Searching boxes for application ${appId}`);
|
|
@@ -141,7 +141,7 @@ export const assetTools = [
|
|
|
141
141
|
})
|
|
142
142
|
}
|
|
143
143
|
];
|
|
144
|
-
export async function lookupAssetByID(assetId, network = '
|
|
144
|
+
export async function lookupAssetByID(assetId, network = 'testnet') {
|
|
145
145
|
try {
|
|
146
146
|
const indexerClient = getIndexerClient(network);
|
|
147
147
|
console.error(`Looking up asset info for ID ${assetId}`);
|
|
@@ -157,7 +157,7 @@ export async function lookupAssetByID(assetId, network = 'mainnet') {
|
|
|
157
157
|
throw new McpError(ErrorCode.InternalError, `Failed to get asset info: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
-
export async function lookupAssetBalances(assetId, params, network = '
|
|
160
|
+
export async function lookupAssetBalances(assetId, params, network = 'testnet') {
|
|
161
161
|
try {
|
|
162
162
|
const indexerClient = getIndexerClient(network);
|
|
163
163
|
console.error(`Looking up balances for asset ${assetId}`);
|
|
@@ -186,7 +186,7 @@ export async function lookupAssetBalances(assetId, params, network = 'mainnet')
|
|
|
186
186
|
throw new McpError(ErrorCode.InternalError, `Failed to get asset balances: ${error instanceof Error ? error.message : String(error)}`);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
export async function lookupAssetTransactions(assetId, params, network = '
|
|
189
|
+
export async function lookupAssetTransactions(assetId, params, network = 'testnet') {
|
|
190
190
|
try {
|
|
191
191
|
const indexerClient = getIndexerClient(network);
|
|
192
192
|
console.error(`Looking up transactions for asset ${assetId}`);
|
|
@@ -230,7 +230,7 @@ export async function lookupAssetTransactions(assetId, params, network = 'mainne
|
|
|
230
230
|
throw new McpError(ErrorCode.InternalError, `Failed to get asset transactions: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
export async function searchForAssets(params, network = '
|
|
233
|
+
export async function searchForAssets(params, network = 'testnet') {
|
|
234
234
|
try {
|
|
235
235
|
const indexerClient = getIndexerClient(network);
|
|
236
236
|
console.error('Searching assets with params:', params);
|
|
@@ -125,7 +125,7 @@ export const transactionTools = [
|
|
|
125
125
|
})
|
|
126
126
|
}
|
|
127
127
|
];
|
|
128
|
-
export async function lookupTransactionByID(txId, network = '
|
|
128
|
+
export async function lookupTransactionByID(txId, network = 'testnet') {
|
|
129
129
|
try {
|
|
130
130
|
const indexerClient = getIndexerClient(network);
|
|
131
131
|
console.error(`Looking up transaction with ID ${txId}`);
|
|
@@ -145,7 +145,7 @@ export async function lookupTransactionByID(txId, network = 'mainnet') {
|
|
|
145
145
|
throw new McpError(ErrorCode.InternalError, `Failed to get transaction: ${error instanceof Error ? error.message : String(error)}`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
export async function lookupAccountTransactions(address, params, network = '
|
|
148
|
+
export async function lookupAccountTransactions(address, params, network = 'testnet') {
|
|
149
149
|
try {
|
|
150
150
|
const indexerClient = getIndexerClient(network);
|
|
151
151
|
console.error(`Looking up transactions for account ${address}`);
|
|
@@ -188,7 +188,7 @@ export async function lookupAccountTransactions(address, params, network = 'main
|
|
|
188
188
|
throw new McpError(ErrorCode.InternalError, `Failed to get account transactions: ${error instanceof Error ? error.message : String(error)}`);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
export async function searchForTransactions(params, network = '
|
|
191
|
+
export async function searchForTransactions(params, network = 'testnet') {
|
|
192
192
|
try {
|
|
193
193
|
const indexerClient = getIndexerClient(network);
|
|
194
194
|
console.error('Searching transactions with params:', params);
|
|
@@ -3,7 +3,7 @@ export const networkParam = {
|
|
|
3
3
|
network: {
|
|
4
4
|
type: 'string',
|
|
5
5
|
enum: ['mainnet', 'testnet', 'localnet'],
|
|
6
|
-
description: 'Algorand network to use (default:
|
|
6
|
+
description: 'Algorand network to use (default: testnet)',
|
|
7
7
|
},
|
|
8
8
|
};
|
|
9
9
|
export const itemsPerPageParam = {
|
|
@@ -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.
|
|
89
|
+
version: '4.2.6',
|
|
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,10 +3,6 @@ export interface AccountRow {
|
|
|
3
3
|
address: string;
|
|
4
4
|
public_key: string;
|
|
5
5
|
nickname: string;
|
|
6
|
-
allowance: number;
|
|
7
|
-
daily_allowance: number;
|
|
8
|
-
daily_spent: number;
|
|
9
|
-
last_spend_date: string;
|
|
10
6
|
created_at: string;
|
|
11
7
|
}
|
|
12
8
|
export declare class WalletManager {
|
|
@@ -24,15 +20,12 @@ export declare class WalletManager {
|
|
|
24
20
|
private static getMnemonic;
|
|
25
21
|
private static deleteMnemonic;
|
|
26
22
|
private static getActiveSecretKey;
|
|
27
|
-
private static checkSpendingLimits;
|
|
28
|
-
private static recordSpend;
|
|
29
23
|
/**
|
|
30
24
|
* Converts flat JSON transaction format (as returned by make_*_txn / assign_group_id)
|
|
31
25
|
* into an algosdk v3 Transaction object. Handles genesisHash in base64 string,
|
|
32
26
|
* Uint8Array, or JSON-round-tripped {0:byte,...} object form.
|
|
33
27
|
*/
|
|
34
28
|
private static prepareTransaction;
|
|
35
|
-
private static extractTxnAmount;
|
|
36
29
|
static handleTool(name: string, args: Record<string, unknown>): Promise<{
|
|
37
30
|
content: {
|
|
38
31
|
type: string;
|
|
@@ -42,12 +35,8 @@ export declare class WalletManager {
|
|
|
42
35
|
static getAccounts(): Promise<AccountRow[]>;
|
|
43
36
|
static hasAccounts(): Promise<boolean>;
|
|
44
37
|
static getActiveAccountInfo(): Promise<AccountRow | null>;
|
|
45
|
-
/** Get active account details (address, nickname
|
|
38
|
+
/** Get active account details (address, nickname). */
|
|
46
39
|
static getActiveWalletAccount(): Promise<AccountRow>;
|
|
47
40
|
/** Get active account's secret key from OS keychain. */
|
|
48
41
|
static getActiveWalletSecretKey(): Promise<Uint8Array>;
|
|
49
|
-
/** Check if transaction amount is within spending limits. Throws on violation. */
|
|
50
|
-
static checkWalletSpendingLimits(account: AccountRow, amountMicroAlgos: number): void;
|
|
51
|
-
/** Record a spend against the account's daily allowance tracker. */
|
|
52
|
-
static recordWalletSpend(address: string, amountMicroAlgos: number): Promise<void>;
|
|
53
42
|
}
|
|
@@ -35,10 +35,6 @@ 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
|
-
allowance INTEGER DEFAULT 0,
|
|
39
|
-
daily_allowance INTEGER DEFAULT 0,
|
|
40
|
-
daily_spent INTEGER DEFAULT 0,
|
|
41
|
-
last_spend_date TEXT DEFAULT '',
|
|
42
38
|
created_at TEXT NOT NULL
|
|
43
39
|
)
|
|
44
40
|
`);
|
|
@@ -67,9 +63,7 @@ const walletToolSchemas = {
|
|
|
67
63
|
addAccount: {
|
|
68
64
|
type: 'object',
|
|
69
65
|
properties: {
|
|
70
|
-
nickname: { type: 'string', description: 'Human-readable nickname for this account' }
|
|
71
|
-
allowance: { type: 'number', description: 'Max per-transaction amount in microAlgos (0 = unlimited)' },
|
|
72
|
-
dailyAllowance: { type: 'number', description: 'Max daily spending total in microAlgos (0 = unlimited)' }
|
|
66
|
+
nickname: { type: 'string', description: 'Human-readable nickname for this account' }
|
|
73
67
|
},
|
|
74
68
|
required: ['nickname']
|
|
75
69
|
},
|
|
@@ -221,35 +215,6 @@ export class WalletManager {
|
|
|
221
215
|
const mnemonic = WalletManager.getMnemonic(account.address);
|
|
222
216
|
return algosdk.mnemonicToSecretKey(mnemonic).sk;
|
|
223
217
|
}
|
|
224
|
-
// ── Spending Limit Enforcement ─────────────────────────────────────────────
|
|
225
|
-
static checkSpendingLimits(account, amountMicroAlgos) {
|
|
226
|
-
if (amountMicroAlgos <= 0)
|
|
227
|
-
return;
|
|
228
|
-
if (account.allowance > 0 && amountMicroAlgos > account.allowance) {
|
|
229
|
-
throw new McpError(ErrorCode.InvalidRequest, `Transaction amount ${amountMicroAlgos} microAlgos exceeds per-transaction allowance of ${account.allowance} for account "${account.nickname}"`);
|
|
230
|
-
}
|
|
231
|
-
if (account.daily_allowance > 0) {
|
|
232
|
-
const today = new Date().toISOString().split('T')[0];
|
|
233
|
-
const spent = account.last_spend_date === today ? account.daily_spent : 0;
|
|
234
|
-
if (spent + amountMicroAlgos > account.daily_allowance) {
|
|
235
|
-
throw new McpError(ErrorCode.InvalidRequest, `Would exceed daily allowance: ${spent} spent + ${amountMicroAlgos} = ${spent + amountMicroAlgos} > limit ${account.daily_allowance} for "${account.nickname}"`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
static async recordSpend(address, amountMicroAlgos) {
|
|
240
|
-
if (amountMicroAlgos <= 0)
|
|
241
|
-
return;
|
|
242
|
-
const database = await getDb();
|
|
243
|
-
const today = new Date().toISOString().split('T')[0];
|
|
244
|
-
// Reset if new day, then add
|
|
245
|
-
database.run(`
|
|
246
|
-
UPDATE accounts SET
|
|
247
|
-
daily_spent = CASE WHEN last_spend_date = ? THEN daily_spent + ? ELSE ? END,
|
|
248
|
-
last_spend_date = ?
|
|
249
|
-
WHERE address = ?
|
|
250
|
-
`, [today, amountMicroAlgos, amountMicroAlgos, today, address]);
|
|
251
|
-
persistDb();
|
|
252
|
-
}
|
|
253
218
|
// ── Transaction Helpers ────────────────────────────────────────────────────
|
|
254
219
|
/**
|
|
255
220
|
* Converts flat JSON transaction format (as returned by make_*_txn / assign_group_id)
|
|
@@ -384,13 +349,6 @@ export class WalletManager {
|
|
|
384
349
|
}
|
|
385
350
|
return txn;
|
|
386
351
|
}
|
|
387
|
-
static extractTxnAmount(txnObj) {
|
|
388
|
-
if (typeof txnObj.amount === 'number')
|
|
389
|
-
return txnObj.amount;
|
|
390
|
-
if (typeof txnObj.amt === 'number')
|
|
391
|
-
return txnObj.amt;
|
|
392
|
-
return 0;
|
|
393
|
-
}
|
|
394
352
|
// ── Tool Handler ───────────────────────────────────────────────────────────
|
|
395
353
|
static async handleTool(name, args) {
|
|
396
354
|
try {
|
|
@@ -420,9 +378,7 @@ export class WalletManager {
|
|
|
420
378
|
}
|
|
421
379
|
// Store mnemonic in OS keychain
|
|
422
380
|
WalletManager.storeMnemonic(address, mnemonic);
|
|
423
|
-
|
|
424
|
-
const dailyAllowance = typeof args.dailyAllowance === 'number' ? args.dailyAllowance : 0;
|
|
425
|
-
database.run('INSERT INTO accounts (address, public_key, nickname, allowance, daily_allowance, daily_spent, last_spend_date, created_at) VALUES (?, ?, ?, ?, ?, 0, \'\', ?)', [address, publicKey, args.nickname, allowance, dailyAllowance, new Date().toISOString()]);
|
|
381
|
+
database.run('INSERT INTO accounts (address, public_key, nickname, created_at) VALUES (?, ?, ?, ?)', [address, publicKey, args.nickname, new Date().toISOString()]);
|
|
426
382
|
persistDb();
|
|
427
383
|
const accounts = await WalletManager.getAllAccounts();
|
|
428
384
|
const newIndex = accounts.findIndex(a => a.address === address);
|
|
@@ -438,8 +394,6 @@ export class WalletManager {
|
|
|
438
394
|
publicKey,
|
|
439
395
|
nickname: args.nickname,
|
|
440
396
|
index: newIndex,
|
|
441
|
-
allowance,
|
|
442
|
-
dailyAllowance,
|
|
443
397
|
}, null, 2),
|
|
444
398
|
}],
|
|
445
399
|
};
|
|
@@ -480,7 +434,6 @@ export class WalletManager {
|
|
|
480
434
|
case 'wallet_list_accounts': {
|
|
481
435
|
const accounts = await WalletManager.getAllAccounts();
|
|
482
436
|
const activeIdx = await WalletManager.getActiveIndex();
|
|
483
|
-
const today = new Date().toISOString().split('T')[0];
|
|
484
437
|
return {
|
|
485
438
|
content: [{
|
|
486
439
|
type: 'text',
|
|
@@ -493,9 +446,6 @@ export class WalletManager {
|
|
|
493
446
|
nickname: a.nickname,
|
|
494
447
|
address: a.address,
|
|
495
448
|
publicKey: a.public_key,
|
|
496
|
-
allowance: a.allowance,
|
|
497
|
-
dailyAllowance: a.daily_allowance,
|
|
498
|
-
dailySpent: a.last_spend_date === today ? a.daily_spent : 0,
|
|
499
449
|
createdAt: a.created_at,
|
|
500
450
|
})),
|
|
501
451
|
}, null, 2),
|
|
@@ -529,7 +479,6 @@ export class WalletManager {
|
|
|
529
479
|
const account = await WalletManager.getActiveAccount();
|
|
530
480
|
const network = extractNetwork(args);
|
|
531
481
|
const algodClient = getAlgodClient(network);
|
|
532
|
-
const today = new Date().toISOString().split('T')[0];
|
|
533
482
|
let onChainInfo = {};
|
|
534
483
|
try {
|
|
535
484
|
const info = await algodClient.accountInformation(account.address).do();
|
|
@@ -554,9 +503,6 @@ export class WalletManager {
|
|
|
554
503
|
publicKey: account.public_key,
|
|
555
504
|
network,
|
|
556
505
|
...onChainInfo,
|
|
557
|
-
allowance: account.allowance,
|
|
558
|
-
dailyAllowance: account.daily_allowance,
|
|
559
|
-
dailySpent: account.last_spend_date === today ? account.daily_spent : 0,
|
|
560
506
|
}, null, 2),
|
|
561
507
|
}],
|
|
562
508
|
};
|
|
@@ -591,12 +537,9 @@ export class WalletManager {
|
|
|
591
537
|
}
|
|
592
538
|
const account = await WalletManager.getActiveAccount();
|
|
593
539
|
const txnObj = args.transaction;
|
|
594
|
-
const amount = WalletManager.extractTxnAmount(txnObj);
|
|
595
|
-
WalletManager.checkSpendingLimits(account, amount);
|
|
596
540
|
const txn = WalletManager.prepareTransaction(txnObj);
|
|
597
541
|
const sk = await WalletManager.getActiveSecretKey();
|
|
598
542
|
const signedTxn = algosdk.signTransaction(txn, sk);
|
|
599
|
-
await WalletManager.recordSpend(account.address, amount);
|
|
600
543
|
return {
|
|
601
544
|
content: [{
|
|
602
545
|
type: 'text',
|
|
@@ -615,11 +558,6 @@ export class WalletManager {
|
|
|
615
558
|
throw new McpError(ErrorCode.InvalidParams, 'Non-empty transactions array is required');
|
|
616
559
|
}
|
|
617
560
|
const account = await WalletManager.getActiveAccount();
|
|
618
|
-
let totalAmount = 0;
|
|
619
|
-
for (const txnObj of args.transactions) {
|
|
620
|
-
totalAmount += WalletManager.extractTxnAmount(txnObj);
|
|
621
|
-
}
|
|
622
|
-
WalletManager.checkSpendingLimits(account, totalAmount);
|
|
623
561
|
const txns = args.transactions.map((txnObj) => WalletManager.prepareTransaction(txnObj));
|
|
624
562
|
const groupedTxns = algosdk.assignGroupID(txns);
|
|
625
563
|
const sk = await WalletManager.getActiveSecretKey();
|
|
@@ -630,7 +568,6 @@ export class WalletManager {
|
|
|
630
568
|
blob: algosdk.bytesToBase64(signed.blob),
|
|
631
569
|
};
|
|
632
570
|
});
|
|
633
|
-
await WalletManager.recordSpend(account.address, totalAmount);
|
|
634
571
|
return {
|
|
635
572
|
content: [{
|
|
636
573
|
type: 'text',
|
|
@@ -726,7 +663,7 @@ export class WalletManager {
|
|
|
726
663
|
}
|
|
727
664
|
}
|
|
728
665
|
// ── Public accessors for external tool integration (e.g., haystack-router) ──
|
|
729
|
-
/** Get active account details (address, nickname
|
|
666
|
+
/** Get active account details (address, nickname). */
|
|
730
667
|
static async getActiveWalletAccount() {
|
|
731
668
|
return WalletManager.getActiveAccount();
|
|
732
669
|
}
|
|
@@ -734,19 +671,11 @@ export class WalletManager {
|
|
|
734
671
|
static async getActiveWalletSecretKey() {
|
|
735
672
|
return WalletManager.getActiveSecretKey();
|
|
736
673
|
}
|
|
737
|
-
/** Check if transaction amount is within spending limits. Throws on violation. */
|
|
738
|
-
static checkWalletSpendingLimits(account, amountMicroAlgos) {
|
|
739
|
-
return WalletManager.checkSpendingLimits(account, amountMicroAlgos);
|
|
740
|
-
}
|
|
741
|
-
/** Record a spend against the account's daily allowance tracker. */
|
|
742
|
-
static async recordWalletSpend(address, amountMicroAlgos) {
|
|
743
|
-
return WalletManager.recordSpend(address, amountMicroAlgos);
|
|
744
|
-
}
|
|
745
674
|
}
|
|
746
675
|
WalletManager.walletTools = [
|
|
747
676
|
{
|
|
748
677
|
name: 'wallet_add_account',
|
|
749
|
-
description: 'Create a new Algorand account and store it securely in the OS keychain with a nickname
|
|
678
|
+
description: 'Create a new Algorand account and store it securely in the OS keychain with a nickname. Returns only address and public key.',
|
|
750
679
|
inputSchema: withCommonParams(walletToolSchemas.addAccount),
|
|
751
680
|
},
|
|
752
681
|
{
|
|
@@ -756,7 +685,7 @@ WalletManager.walletTools = [
|
|
|
756
685
|
},
|
|
757
686
|
{
|
|
758
687
|
name: 'wallet_list_accounts',
|
|
759
|
-
description: 'List all wallet accounts with their nicknames
|
|
688
|
+
description: 'List all wallet accounts with their nicknames and addresses.',
|
|
760
689
|
inputSchema: withCommonParams(walletToolSchemas.listAccounts),
|
|
761
690
|
},
|
|
762
691
|
{
|
|
@@ -766,7 +695,7 @@ WalletManager.walletTools = [
|
|
|
766
695
|
},
|
|
767
696
|
{
|
|
768
697
|
name: 'wallet_get_info',
|
|
769
|
-
description: 'Get info for the ACTIVE wallet account (an account whose private key this MCP server owns in the OS keychain). Returns nickname, address, public key, network, on-chain balance, min-balance, opted-in apps/assets counts
|
|
698
|
+
description: 'Get info for the ACTIVE wallet account (an account whose private key this MCP server owns in the OS keychain). Returns nickname, address, public key, network, on-chain balance, min-balance, opted-in apps/assets counts. Use this when you want to know the state of "the wallet you control." For an arbitrary on-chain account that this wallet does NOT own, use api_algod_get_account_info instead.',
|
|
770
699
|
inputSchema: withCommonParams(walletToolSchemas.getInfo),
|
|
771
700
|
},
|
|
772
701
|
{
|
|
@@ -776,12 +705,12 @@ WalletManager.walletTools = [
|
|
|
776
705
|
},
|
|
777
706
|
{
|
|
778
707
|
name: 'wallet_sign_transaction',
|
|
779
|
-
description: 'Sign a single transaction with the active wallet account.
|
|
708
|
+
description: 'Sign a single transaction with the active wallet account.',
|
|
780
709
|
inputSchema: withCommonParams(walletToolSchemas.signTransaction),
|
|
781
710
|
},
|
|
782
711
|
{
|
|
783
712
|
name: 'wallet_sign_transaction_group',
|
|
784
|
-
description: 'Sign a group of transactions with the active wallet account. Assigns group ID automatically
|
|
713
|
+
description: 'Sign a group of transactions with the active wallet account. Assigns group ID automatically.',
|
|
785
714
|
inputSchema: withCommonParams(walletToolSchemas.signTransactionGroup),
|
|
786
715
|
},
|
|
787
716
|
{
|