@goplausible/algorand-mcp 3.0.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -15
- package/dist/tools/apiManager/hayrouter/index.d.ts +3 -0
- package/dist/tools/apiManager/hayrouter/index.js +35 -0
- package/dist/tools/apiManager/hayrouter/optin.d.ts +3 -0
- package/dist/tools/apiManager/hayrouter/optin.js +47 -0
- package/dist/tools/apiManager/hayrouter/quote.d.ts +3 -0
- package/dist/tools/apiManager/hayrouter/quote.js +101 -0
- package/dist/tools/apiManager/hayrouter/routerClient.d.ts +2 -0
- package/dist/tools/apiManager/hayrouter/routerClient.js +28 -0
- package/dist/tools/apiManager/hayrouter/swap.d.ts +3 -0
- package/dist/tools/apiManager/hayrouter/swap.js +151 -0
- package/dist/tools/apiManager/index.js +6 -0
- package/dist/tools/walletManager.d.ts +9 -2
- package/dist/tools/walletManager.js +17 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Algorand MCP Server
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/algorand-mcp)
|
|
4
|
-
[](https://badge.fury.io/js/algorand-mcp)
|
|
2
|
+
[](https://www.npmjs.com/package/@goplausible/algorand-mcp)
|
|
3
|
+
[](https://www.npmjs.com/package/@goplausible/algorand-mcp)
|
|
5
4
|
[](https://opensource.org/licenses/MIT)
|
|
6
5
|
|
|
7
6
|
A comprehensive [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that gives AI agents and LLMs full access to the Algorand blockchain. Built by [GoPlausible](https://goplausible.com).
|
|
@@ -15,12 +14,14 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
|
|
|
15
14
|
## Features
|
|
16
15
|
|
|
17
16
|
- Secure wallet management via OS keychain — private keys never exposed to agents or LLMs
|
|
17
|
+
- Wallet accounts nicknames, allowances, and daily limits for safe spending control
|
|
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
|
|
21
21
|
- TEAL compilation and disassembly
|
|
22
22
|
- Full Algod and Indexer API access
|
|
23
23
|
- NFDomains (NFD) name service integration
|
|
24
|
+
- x402 and AP2 toolins for Algorand
|
|
24
25
|
- Tinyman AMM integration (pools, swaps, liquidity)
|
|
25
26
|
- ARC-26 URI and QR code generation
|
|
26
27
|
- Algorand knowledge base with full developer documentation taxonomy
|
|
@@ -36,7 +37,7 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
|
|
|
36
37
|
### From npm
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
|
-
npm install -g algorand-mcp
|
|
40
|
+
npm install -g @goplausible/algorand-mcp
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
### From source
|
|
@@ -50,12 +51,56 @@ npm run build
|
|
|
50
51
|
|
|
51
52
|
## MCP Configuration
|
|
52
53
|
|
|
53
|
-
The server runs over stdio
|
|
54
|
+
The server runs over **stdio**. There are three ways to invoke it — pick whichever suits your setup:
|
|
55
|
+
|
|
56
|
+
| Method | Command | When to use |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| **npx** (recommended) | `npx @goplausible/algorand-mcp` | No install needed, always latest version |
|
|
59
|
+
| **Global install** | `algorand-mcp` | After `npm install -g @goplausible/algorand-mcp` |
|
|
60
|
+
| **Absolute path** | `node /path/to/dist/index.js` | Built from source or local clone |
|
|
61
|
+
|
|
62
|
+
**No environment variables are required** for standard use. Network selection, pagination, and node URLs are all handled dynamically per tool call.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### OpenClaw
|
|
67
|
+
|
|
68
|
+
No manual configuration needed — install the [`@goplausible/openclaw-algorand-plugin`](https://www.npmjs.com/package/@goplausible/openclaw-algorand-plugin) npm package and the Algorand MCP server is configured automatically:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install -g @goplausible/openclaw-algorand-plugin
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
54
75
|
|
|
55
76
|
### Claude Desktop
|
|
56
77
|
|
|
57
78
|
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
58
79
|
|
|
80
|
+
**Using npx:**
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"algorand-mcp": {
|
|
85
|
+
"command": "npx",
|
|
86
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Using global install:**
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"algorand-mcp": {
|
|
97
|
+
"command": "algorand-mcp"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Using absolute path:**
|
|
59
104
|
```json
|
|
60
105
|
{
|
|
61
106
|
"mcpServers": {
|
|
@@ -67,41 +112,143 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
|
|
|
67
112
|
}
|
|
68
113
|
```
|
|
69
114
|
|
|
115
|
+
---
|
|
116
|
+
|
|
70
117
|
### Claude Code
|
|
71
118
|
|
|
72
|
-
Create `.mcp.json` in your project root (project scope) or
|
|
119
|
+
Create `.mcp.json` in your project root (project scope) or `~/.claude.json` (user scope):
|
|
73
120
|
|
|
74
121
|
```json
|
|
75
122
|
{
|
|
76
123
|
"mcpServers": {
|
|
77
124
|
"algorand-mcp": {
|
|
78
125
|
"type": "stdio",
|
|
79
|
-
"command": "
|
|
80
|
-
"args": ["/
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
81
128
|
}
|
|
82
129
|
}
|
|
83
130
|
}
|
|
84
131
|
```
|
|
85
132
|
|
|
86
|
-
|
|
133
|
+
Or add interactively:
|
|
134
|
+
```bash
|
|
135
|
+
claude mcp add algorand-mcp -- npx @goplausible/algorand-mcp
|
|
136
|
+
```
|
|
87
137
|
|
|
88
|
-
|
|
138
|
+
---
|
|
89
139
|
|
|
90
|
-
###
|
|
140
|
+
### Cursor
|
|
91
141
|
|
|
92
|
-
|
|
142
|
+
Add via **Settings > MCP Servers**, or edit `.cursor/mcp.json` in your project root:
|
|
93
143
|
|
|
94
144
|
```json
|
|
95
145
|
{
|
|
96
146
|
"mcpServers": {
|
|
97
147
|
"algorand-mcp": {
|
|
98
|
-
"command": "
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
99
150
|
}
|
|
100
151
|
}
|
|
101
152
|
}
|
|
102
153
|
```
|
|
103
154
|
|
|
104
|
-
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Windsurf
|
|
158
|
+
|
|
159
|
+
Add via **Settings > MCP**, or edit `~/.codeium/windsurf/mcp_config.json`:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"mcpServers": {
|
|
164
|
+
"algorand-mcp": {
|
|
165
|
+
"command": "npx",
|
|
166
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### VS Code / GitHub Copilot
|
|
175
|
+
|
|
176
|
+
Edit `.vscode/mcp.json` in your workspace root, or open **Settings > MCP Servers**:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"servers": {
|
|
181
|
+
"algorand-mcp": {
|
|
182
|
+
"type": "stdio",
|
|
183
|
+
"command": "npx",
|
|
184
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Cline
|
|
193
|
+
|
|
194
|
+
Add via the **MCP Servers** panel in the Cline sidebar, or edit `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` (macOS):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"mcpServers": {
|
|
199
|
+
"algorand-mcp": {
|
|
200
|
+
"command": "npx",
|
|
201
|
+
"args": ["@goplausible/algorand-mcp"],
|
|
202
|
+
"disabled": false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### OpenAI Codex CLI
|
|
211
|
+
|
|
212
|
+
Create `.codex/mcp.json` in your project root or `~/.codex/mcp.json` for global scope:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"mcpServers": {
|
|
217
|
+
"algorand-mcp": {
|
|
218
|
+
"command": "npx",
|
|
219
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### Open Code
|
|
228
|
+
|
|
229
|
+
Edit `~/.config/opencode/config.json`:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"mcp": {
|
|
234
|
+
"algorand-mcp": {
|
|
235
|
+
"type": "stdio",
|
|
236
|
+
"command": "npx",
|
|
237
|
+
"args": ["@goplausible/algorand-mcp"]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### Any MCP-compatible client
|
|
246
|
+
|
|
247
|
+
The server speaks the standard MCP stdio protocol. For any client not listed above, configure it with:
|
|
248
|
+
|
|
249
|
+
- **Command:** `npx` (or `algorand-mcp` if globally installed, or `node /path/to/dist/index.js`)
|
|
250
|
+
- **Args:** `["@goplausible/algorand-mcp"]` (for npx)
|
|
251
|
+
- **Transport:** `stdio`
|
|
105
252
|
|
|
106
253
|
## Network Selection
|
|
107
254
|
|
|
@@ -256,7 +403,7 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
256
403
|
| `encode_obj` | Encode object to msgpack |
|
|
257
404
|
| `decode_obj` | Decode msgpack to object |
|
|
258
405
|
|
|
259
|
-
### Transaction Tools (
|
|
406
|
+
### Transaction Tools (18 tools)
|
|
260
407
|
|
|
261
408
|
| Tool | Description |
|
|
262
409
|
|---|---|
|
|
@@ -276,6 +423,8 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
276
423
|
| `make_app_call_txn` | Create an application call transaction |
|
|
277
424
|
| `assign_group_id` | Assign group ID for atomic transactions |
|
|
278
425
|
| `sign_transaction` | Sign a transaction with a secret key |
|
|
426
|
+
| `encode_unsigned_transaction` | Encode an unsigned transaction to base64 msgpack bytes |
|
|
427
|
+
| `decode_signed_transaction` | Decode a signed transaction blob back to JSON with signature details |
|
|
279
428
|
|
|
280
429
|
### Algod Tools (5 tools)
|
|
281
430
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { quoteTools, handleQuoteTools } from './quote.js';
|
|
3
|
+
import { swapTools, handleSwapTools } from './swap.js';
|
|
4
|
+
import { optinTools, handleOptinTools } from './optin.js';
|
|
5
|
+
// Combine all Haystack Router tools
|
|
6
|
+
export const haystackTools = [
|
|
7
|
+
...quoteTools,
|
|
8
|
+
...swapTools,
|
|
9
|
+
...optinTools,
|
|
10
|
+
];
|
|
11
|
+
// Handle all Haystack Router tools
|
|
12
|
+
export async function handleHaystackTools(name, args) {
|
|
13
|
+
try {
|
|
14
|
+
const combinedArgs = { name, ...args };
|
|
15
|
+
// Swap execution (must come before quote due to prefix matching)
|
|
16
|
+
if (name === 'api_haystack_execute_swap') {
|
|
17
|
+
return handleSwapTools(combinedArgs);
|
|
18
|
+
}
|
|
19
|
+
// Quote tools
|
|
20
|
+
if (name === 'api_haystack_get_swap_quote') {
|
|
21
|
+
return handleQuoteTools(combinedArgs);
|
|
22
|
+
}
|
|
23
|
+
// Opt-in check
|
|
24
|
+
if (name === 'api_haystack_needs_optin') {
|
|
25
|
+
return handleOptinTools(combinedArgs);
|
|
26
|
+
}
|
|
27
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown Haystack Router tool: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof McpError) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
throw new McpError(ErrorCode.InternalError, `Failed to handle Haystack Router tool: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { extractNetwork } from '../../../algorand-client.js';
|
|
3
|
+
import { withCommonParams } from '../../commonParams.js';
|
|
4
|
+
import { getRouterClient } from './routerClient.js';
|
|
5
|
+
export const optinTools = [
|
|
6
|
+
{
|
|
7
|
+
name: 'api_haystack_needs_optin',
|
|
8
|
+
description: 'Check if an Algorand address needs to opt into an asset before swapping. Returns true if opt-in is needed, false otherwise. Always returns false for ALGO (ASA 0). Use before executing a swap to determine if wallet_optin_asset should be called first.',
|
|
9
|
+
inputSchema: withCommonParams({
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
address: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Algorand address to check'
|
|
15
|
+
},
|
|
16
|
+
assetId: {
|
|
17
|
+
type: 'integer',
|
|
18
|
+
description: 'Asset ID to check opt-in status for'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ['address', 'assetId']
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
export async function handleOptinTools(args) {
|
|
26
|
+
const { name, address, assetId } = args;
|
|
27
|
+
const network = extractNetwork(args);
|
|
28
|
+
if (network === 'localnet') {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Haystack Router is not available on localnet');
|
|
30
|
+
}
|
|
31
|
+
if (name === 'api_haystack_needs_optin') {
|
|
32
|
+
try {
|
|
33
|
+
const router = getRouterClient(network);
|
|
34
|
+
const needsOptIn = await router.needsAssetOptIn(address, assetId);
|
|
35
|
+
return {
|
|
36
|
+
address,
|
|
37
|
+
assetId,
|
|
38
|
+
needsOptIn,
|
|
39
|
+
network,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new McpError(ErrorCode.InternalError, `Failed to check asset opt-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown Haystack opt-in tool: ${name}`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { extractNetwork } from '../../../algorand-client.js';
|
|
3
|
+
import { withCommonParams } from '../../commonParams.js';
|
|
4
|
+
import { getRouterClient } from './routerClient.js';
|
|
5
|
+
export const quoteTools = [
|
|
6
|
+
{
|
|
7
|
+
name: 'api_haystack_get_swap_quote',
|
|
8
|
+
description: 'Get an optimized swap quote from Haystack Router — a DEX aggregator that finds the best swap route across multiple Algorand DEXes (Tinyman V2, Pact, Folks) and LST protocols (tALGO, xALGO). Returns the best-price quote with route details, USD values, and price impact. Use this to preview a swap before executing. All amounts are in base units (e.g., 1000000 = 1 ALGO).',
|
|
9
|
+
inputSchema: withCommonParams({
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
fromASAID: {
|
|
13
|
+
type: 'integer',
|
|
14
|
+
description: 'Input asset ID (0 = ALGO, 31566704 = USDC, 312769 = USDt, etc.)'
|
|
15
|
+
},
|
|
16
|
+
toASAID: {
|
|
17
|
+
type: 'integer',
|
|
18
|
+
description: 'Output asset ID (0 = ALGO, 31566704 = USDC, 312769 = USDt, etc.)'
|
|
19
|
+
},
|
|
20
|
+
amount: {
|
|
21
|
+
type: 'integer',
|
|
22
|
+
description: 'Amount in base units (e.g., 1000000 = 1 ALGO with 6 decimals)'
|
|
23
|
+
},
|
|
24
|
+
type: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['fixed-input', 'fixed-output'],
|
|
27
|
+
description: 'Quote type: fixed-input (specify input amount, default) or fixed-output (specify desired output amount)',
|
|
28
|
+
default: 'fixed-input'
|
|
29
|
+
},
|
|
30
|
+
address: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'User Algorand address (optional, needed for auto opt-in detection)'
|
|
33
|
+
},
|
|
34
|
+
maxGroupSize: {
|
|
35
|
+
type: 'integer',
|
|
36
|
+
description: 'Maximum transactions in atomic group (default: 16)',
|
|
37
|
+
default: 16
|
|
38
|
+
},
|
|
39
|
+
maxDepth: {
|
|
40
|
+
type: 'integer',
|
|
41
|
+
description: 'Maximum routing hops (default: 4)',
|
|
42
|
+
default: 4
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
required: ['fromASAID', 'toASAID', 'amount']
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
export async function handleQuoteTools(args) {
|
|
50
|
+
const { name, fromASAID, toASAID, amount, type = 'fixed-input', address, maxGroupSize, maxDepth, } = args;
|
|
51
|
+
const network = extractNetwork(args);
|
|
52
|
+
if (network === 'localnet') {
|
|
53
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Haystack Router is not available on localnet');
|
|
54
|
+
}
|
|
55
|
+
const router = getRouterClient(network);
|
|
56
|
+
if (name === 'api_haystack_get_swap_quote') {
|
|
57
|
+
try {
|
|
58
|
+
const quoteParams = {
|
|
59
|
+
fromASAID,
|
|
60
|
+
toASAID,
|
|
61
|
+
amount: BigInt(amount),
|
|
62
|
+
type,
|
|
63
|
+
};
|
|
64
|
+
if (address) {
|
|
65
|
+
quoteParams.address = address;
|
|
66
|
+
}
|
|
67
|
+
if (maxGroupSize !== undefined) {
|
|
68
|
+
quoteParams.maxGroupSize = maxGroupSize;
|
|
69
|
+
}
|
|
70
|
+
if (maxDepth !== undefined) {
|
|
71
|
+
quoteParams.maxDepth = maxDepth;
|
|
72
|
+
}
|
|
73
|
+
const quote = await router.newQuote(quoteParams);
|
|
74
|
+
// Serialize the quote response for MCP transport (convert BigInt to string)
|
|
75
|
+
const result = {
|
|
76
|
+
expectedOutput: quote.quote.toString(),
|
|
77
|
+
inputAmount: quote.amount.toString(),
|
|
78
|
+
fromASAID: quote.fromASAID,
|
|
79
|
+
toASAID: quote.toASAID,
|
|
80
|
+
type: quote.type,
|
|
81
|
+
usdIn: quote.usdIn,
|
|
82
|
+
usdOut: quote.usdOut,
|
|
83
|
+
userPriceImpact: quote.userPriceImpact,
|
|
84
|
+
marketPriceImpact: quote.marketPriceImpact,
|
|
85
|
+
route: quote.route,
|
|
86
|
+
flattenedRoute: quote.flattenedRoute,
|
|
87
|
+
requiredAppOptIns: quote.requiredAppOptIns,
|
|
88
|
+
protocolFees: quote.protocolFees,
|
|
89
|
+
createdAt: quote.createdAt,
|
|
90
|
+
};
|
|
91
|
+
if (quote.address) {
|
|
92
|
+
result.address = quote.address;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get Haystack swap quote: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown Haystack Router tool: ${name}`);
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RouterClient } from '@txnlab/haystack-router';
|
|
2
|
+
// Free tier API key (60 requests/min)
|
|
3
|
+
const HAYSTACK_API_KEY = process.env.HAYSTACK_API_KEY || '1b72df7e-1131-4449-8ce1-29b79dd3f51e';
|
|
4
|
+
// Algod URIs per network for RouterClient configuration
|
|
5
|
+
const ALGOD_URIS = {
|
|
6
|
+
mainnet: 'https://mainnet-api.algonode.cloud',
|
|
7
|
+
testnet: 'https://testnet-api.algonode.cloud',
|
|
8
|
+
};
|
|
9
|
+
// Memoized RouterClient instances per network
|
|
10
|
+
const routerClients = new Map();
|
|
11
|
+
export function getRouterClient(network) {
|
|
12
|
+
let client = routerClients.get(network);
|
|
13
|
+
if (!client) {
|
|
14
|
+
const config = {
|
|
15
|
+
apiKey: HAYSTACK_API_KEY,
|
|
16
|
+
autoOptIn: true,
|
|
17
|
+
};
|
|
18
|
+
if (network === 'testnet') {
|
|
19
|
+
config.algodUri = ALGOD_URIS.testnet;
|
|
20
|
+
}
|
|
21
|
+
else if (network === 'mainnet') {
|
|
22
|
+
config.algodUri = ALGOD_URIS.mainnet;
|
|
23
|
+
}
|
|
24
|
+
client = new RouterClient(config);
|
|
25
|
+
routerClients.set(network, client);
|
|
26
|
+
}
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import algosdk from 'algosdk';
|
|
3
|
+
import { extractNetwork } from '../../../algorand-client.js';
|
|
4
|
+
import { WalletManager } from '../../walletManager.js';
|
|
5
|
+
import { withCommonParams } from '../../commonParams.js';
|
|
6
|
+
import { getRouterClient } from './routerClient.js';
|
|
7
|
+
export const swapTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'api_haystack_execute_swap',
|
|
10
|
+
description: 'Execute an optimized token swap via Haystack Router — gets the best route across multiple DEXes (Tinyman V2, Pact, Folks) and LST protocols, then signs and submits the atomic transaction group using the active wallet account. This is an all-in-one tool: quote → sign → submit → confirm. All amounts are in base units (e.g., 1000000 = 1 ALGO).',
|
|
11
|
+
inputSchema: withCommonParams({
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
fromASAID: {
|
|
15
|
+
type: 'integer',
|
|
16
|
+
description: 'Input asset ID (0 = ALGO, 31566704 = USDC, 312769 = USDt, etc.)'
|
|
17
|
+
},
|
|
18
|
+
toASAID: {
|
|
19
|
+
type: 'integer',
|
|
20
|
+
description: 'Output asset ID (0 = ALGO, 31566704 = USDC, 312769 = USDt, etc.)'
|
|
21
|
+
},
|
|
22
|
+
amount: {
|
|
23
|
+
type: 'integer',
|
|
24
|
+
description: 'Amount in base units (e.g., 1000000 = 1 ALGO with 6 decimals)'
|
|
25
|
+
},
|
|
26
|
+
slippage: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'Slippage tolerance percentage (e.g., 1 = 1%). Recommended: 0.5-1% stable pairs, 1-3% volatile, 3-5% low liquidity',
|
|
29
|
+
default: 1
|
|
30
|
+
},
|
|
31
|
+
type: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
enum: ['fixed-input', 'fixed-output'],
|
|
34
|
+
description: 'Quote type: fixed-input (specify input, default) or fixed-output (specify desired output)',
|
|
35
|
+
default: 'fixed-input'
|
|
36
|
+
},
|
|
37
|
+
note: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Optional note to attach to the input transaction (plain text)'
|
|
40
|
+
},
|
|
41
|
+
maxGroupSize: {
|
|
42
|
+
type: 'integer',
|
|
43
|
+
description: 'Maximum transactions in atomic group (default: 16)',
|
|
44
|
+
default: 16
|
|
45
|
+
},
|
|
46
|
+
maxDepth: {
|
|
47
|
+
type: 'integer',
|
|
48
|
+
description: 'Maximum routing hops (default: 4)',
|
|
49
|
+
default: 4
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required: ['fromASAID', 'toASAID', 'amount']
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
export async function handleSwapTools(args) {
|
|
57
|
+
const { name, fromASAID, toASAID, amount, slippage = 1, type = 'fixed-input', note, maxGroupSize, maxDepth, } = args;
|
|
58
|
+
const network = extractNetwork(args);
|
|
59
|
+
if (network === 'localnet') {
|
|
60
|
+
throw new McpError(ErrorCode.InvalidRequest, 'Haystack Router is not available on localnet');
|
|
61
|
+
}
|
|
62
|
+
if (name === 'api_haystack_execute_swap') {
|
|
63
|
+
try {
|
|
64
|
+
// 1. Get active wallet account and secret key
|
|
65
|
+
const account = await WalletManager.getActiveWalletAccount();
|
|
66
|
+
const sk = await WalletManager.getActiveWalletSecretKey();
|
|
67
|
+
const address = account.address;
|
|
68
|
+
// 2. Create a signer using the wallet's secret key
|
|
69
|
+
const signer = async (txnGroup, indexesToSign) => {
|
|
70
|
+
return indexesToSign.map((index) => algosdk.signTransaction(txnGroup[index], sk).blob);
|
|
71
|
+
};
|
|
72
|
+
// 3. Get router client and fetch quote
|
|
73
|
+
const router = getRouterClient(network);
|
|
74
|
+
const quoteParams = {
|
|
75
|
+
fromASAID,
|
|
76
|
+
toASAID,
|
|
77
|
+
amount: BigInt(amount),
|
|
78
|
+
type,
|
|
79
|
+
address,
|
|
80
|
+
};
|
|
81
|
+
if (maxGroupSize !== undefined)
|
|
82
|
+
quoteParams.maxGroupSize = maxGroupSize;
|
|
83
|
+
if (maxDepth !== undefined)
|
|
84
|
+
quoteParams.maxDepth = maxDepth;
|
|
85
|
+
const quote = await router.newQuote(quoteParams);
|
|
86
|
+
// 4. Check spending limits (estimate based on input amount for ALGO sends)
|
|
87
|
+
const estimatedSpend = fromASAID === 0 ? Number(amount) : 0;
|
|
88
|
+
WalletManager.checkWalletSpendingLimits(account, estimatedSpend);
|
|
89
|
+
// 5. Build and execute swap
|
|
90
|
+
const swapConfig = {
|
|
91
|
+
quote,
|
|
92
|
+
address,
|
|
93
|
+
signer,
|
|
94
|
+
slippage,
|
|
95
|
+
};
|
|
96
|
+
if (note) {
|
|
97
|
+
swapConfig.note = new TextEncoder().encode(note);
|
|
98
|
+
}
|
|
99
|
+
const swap = await router.newSwap(swapConfig);
|
|
100
|
+
const result = await swap.execute();
|
|
101
|
+
// 6. Record spend
|
|
102
|
+
await WalletManager.recordWalletSpend(address, estimatedSpend);
|
|
103
|
+
// 7. Get swap summary
|
|
104
|
+
const summary = swap.getSummary();
|
|
105
|
+
const inputTxnId = swap.getInputTransactionId();
|
|
106
|
+
// 8. Build response
|
|
107
|
+
const response = {
|
|
108
|
+
status: 'confirmed',
|
|
109
|
+
confirmedRound: result.confirmedRound.toString(),
|
|
110
|
+
txIds: result.txIds,
|
|
111
|
+
signer: address,
|
|
112
|
+
nickname: account.nickname,
|
|
113
|
+
network,
|
|
114
|
+
quote: {
|
|
115
|
+
fromASAID: quote.fromASAID,
|
|
116
|
+
toASAID: quote.toASAID,
|
|
117
|
+
expectedOutput: quote.quote.toString(),
|
|
118
|
+
inputAmount: quote.amount.toString(),
|
|
119
|
+
type: quote.type,
|
|
120
|
+
usdIn: quote.usdIn,
|
|
121
|
+
usdOut: quote.usdOut,
|
|
122
|
+
userPriceImpact: quote.userPriceImpact,
|
|
123
|
+
route: quote.flattenedRoute,
|
|
124
|
+
},
|
|
125
|
+
slippage,
|
|
126
|
+
};
|
|
127
|
+
if (summary) {
|
|
128
|
+
response.summary = {
|
|
129
|
+
inputAssetId: summary.inputAssetId.toString(),
|
|
130
|
+
outputAssetId: summary.outputAssetId.toString(),
|
|
131
|
+
inputAmount: summary.inputAmount.toString(),
|
|
132
|
+
outputAmount: summary.outputAmount.toString(),
|
|
133
|
+
totalFees: summary.totalFees.toString(),
|
|
134
|
+
transactionCount: summary.transactionCount,
|
|
135
|
+
inputTxnId: summary.inputTxnId,
|
|
136
|
+
outputTxnId: summary.outputTxnId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (inputTxnId) {
|
|
140
|
+
response.inputTransactionId = inputTxnId;
|
|
141
|
+
}
|
|
142
|
+
return response;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof McpError)
|
|
146
|
+
throw error;
|
|
147
|
+
throw new McpError(ErrorCode.InternalError, `Haystack swap failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown Haystack swap tool: ${name}`);
|
|
151
|
+
}
|
|
@@ -5,6 +5,7 @@ import { nfdTools, handleNFDTools } from './nfd/index.js';
|
|
|
5
5
|
import { tinymanTools, handleTinymanTools } from './tinyman/index.js';
|
|
6
6
|
// import { ultradeTools, handleUltradeTools } from './ultrade/index.js';
|
|
7
7
|
import { exampleTools, handleExampleTools } from './example/index.js';
|
|
8
|
+
import { haystackTools, handleHaystackTools } from './hayrouter/index.js';
|
|
8
9
|
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
9
10
|
import { ResponseProcessor } from '../../utils/responseProcessor.js';
|
|
10
11
|
// Combine all API tools
|
|
@@ -13,6 +14,7 @@ export const apiManager = [
|
|
|
13
14
|
...indexerTools,
|
|
14
15
|
...nfdTools,
|
|
15
16
|
...tinymanTools,
|
|
17
|
+
...haystackTools,
|
|
16
18
|
...exampleTools
|
|
17
19
|
];
|
|
18
20
|
// Handle all API tools
|
|
@@ -35,6 +37,10 @@ export async function handleApiManager(name, args) {
|
|
|
35
37
|
else if (name.startsWith('api_algod_')) {
|
|
36
38
|
response = await handleAlgodTools(name, args);
|
|
37
39
|
}
|
|
40
|
+
// Haystack Router tools
|
|
41
|
+
else if (name.startsWith('api_haystack_')) {
|
|
42
|
+
response = await handleHaystackTools(name, args);
|
|
43
|
+
}
|
|
38
44
|
else if (name.startsWith('api_example_')) {
|
|
39
45
|
response = await handleExampleTools(name, args);
|
|
40
46
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface AccountRow {
|
|
1
|
+
export interface AccountRow {
|
|
2
2
|
id: number;
|
|
3
3
|
address: string;
|
|
4
4
|
public_key: string;
|
|
@@ -42,5 +42,12 @@ export declare class WalletManager {
|
|
|
42
42
|
static getAccounts(): Promise<AccountRow[]>;
|
|
43
43
|
static hasAccounts(): Promise<boolean>;
|
|
44
44
|
static getActiveAccountInfo(): Promise<AccountRow | null>;
|
|
45
|
+
/** Get active account details (address, nickname, spending limits). */
|
|
46
|
+
static getActiveWalletAccount(): Promise<AccountRow>;
|
|
47
|
+
/** Get active account's secret key from OS keychain. */
|
|
48
|
+
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>;
|
|
45
53
|
}
|
|
46
|
-
export {};
|
|
@@ -726,6 +726,23 @@ export class WalletManager {
|
|
|
726
726
|
return null;
|
|
727
727
|
}
|
|
728
728
|
}
|
|
729
|
+
// ── Public accessors for external tool integration (e.g., haystack-router) ──
|
|
730
|
+
/** Get active account details (address, nickname, spending limits). */
|
|
731
|
+
static async getActiveWalletAccount() {
|
|
732
|
+
return WalletManager.getActiveAccount();
|
|
733
|
+
}
|
|
734
|
+
/** Get active account's secret key from OS keychain. */
|
|
735
|
+
static async getActiveWalletSecretKey() {
|
|
736
|
+
return WalletManager.getActiveSecretKey();
|
|
737
|
+
}
|
|
738
|
+
/** Check if transaction amount is within spending limits. Throws on violation. */
|
|
739
|
+
static checkWalletSpendingLimits(account, amountMicroAlgos) {
|
|
740
|
+
return WalletManager.checkSpendingLimits(account, amountMicroAlgos);
|
|
741
|
+
}
|
|
742
|
+
/** Record a spend against the account's daily allowance tracker. */
|
|
743
|
+
static async recordWalletSpend(address, amountMicroAlgos) {
|
|
744
|
+
return WalletManager.recordSpend(address, amountMicroAlgos);
|
|
745
|
+
}
|
|
729
746
|
}
|
|
730
747
|
WalletManager.walletTools = [
|
|
731
748
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplausible/algorand-mcp",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@napi-rs/keyring": "^1.2.0",
|
|
40
40
|
"@noble/curves": "^2.0.1",
|
|
41
41
|
"@tinymanorg/tinyman-js-sdk": "^5.1.2",
|
|
42
|
+
"@txnlab/haystack-router": "^2.0.5",
|
|
42
43
|
"algo-msgpack-with-bigint": "^2.1.1",
|
|
43
44
|
"algosdk": "^3.5.2",
|
|
44
45
|
"hi-base32": "^0.5.1",
|