@ecadlabs/tezosx-mcp 1.0.0 → 1.0.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 +87 -66
- package/dist/api.d.ts +3 -0
- package/dist/api.js +155 -0
- package/dist/config-store.d.ts +15 -0
- package/dist/config-store.js +38 -0
- package/dist/index.d.ts +0 -6
- package/dist/index.js +43 -44
- package/dist/live-config.d.ts +32 -0
- package/dist/live-config.js +55 -0
- package/dist/tools/create_x402_payment.d.ts +2 -2
- package/dist/tools/create_x402_payment.js +2 -1
- package/dist/tools/fetch_with_x402.d.ts +3 -3
- package/dist/tools/fetch_with_x402.js +7 -6
- package/dist/tools/get_addresses.d.ts +2 -2
- package/dist/tools/get_addresses.js +2 -1
- package/dist/tools/get_balance.d.ts +2 -2
- package/dist/tools/get_balance.js +2 -1
- package/dist/tools/get_dashboard.d.ts +2 -1
- package/dist/tools/get_dashboard.js +2 -2
- package/dist/tools/get_limits.d.ts +2 -2
- package/dist/tools/get_limits.js +2 -1
- package/dist/tools/get_operation_history.d.ts +2 -1
- package/dist/tools/get_operation_history.js +2 -1
- package/dist/tools/index.d.ts +3 -3
- package/dist/tools/index.js +15 -24
- package/dist/tools/reveal_account.d.ts +2 -1
- package/dist/tools/reveal_account.js +2 -1
- package/dist/tools/send_xtz.d.ts +2 -9
- package/dist/tools/send_xtz.js +33 -15
- package/dist/webserver.d.ts +2 -1
- package/dist/webserver.js +11 -4
- package/frontend/dist/assets/{index-RtTL1nIl.js → index-B-2-_lot.js} +95 -65
- package/frontend/dist/assets/index-CTdz8_ps.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +2 -1
- package/dist/adapters/index.d.ts +0 -37
- package/dist/adapters/index.js +0 -57
- package/dist/adapters/node.d.ts +0 -18
- package/dist/adapters/node.js +0 -35
- package/dist/adapters/types.d.ts +0 -52
- package/dist/adapters/types.js +0 -25
- package/dist/adapters/worker.d.ts +0 -35
- package/dist/adapters/worker.js +0 -50
- package/dist/server.d.ts +0 -36
- package/dist/server.js +0 -80
- package/dist/tools/get_address.d.ts +0 -20
- package/dist/tools/get_address.js +0 -24
- package/dist/worker.bundle.js +0 -134265
- package/dist/worker.d.ts +0 -13
- package/dist/worker.js +0 -132
- package/frontend/dist/assets/index-mSsI3AqQ.css +0 -1
package/README.md
CHANGED
|
@@ -2,44 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol server for Tezos with x402 payment support.
|
|
4
4
|
|
|
5
|
-
> **Warning:** This MCP is in
|
|
5
|
+
> **Warning:** This MCP is in beta. Most things should work, but please be prepared for troubleshooting. Please report any problems on our [issues page](https://github.com/ecadlabs/TezosX-mcp/issues).
|
|
6
6
|
>
|
|
7
7
|
> As always, verify the output of your LLM before approving any transactions. Set reasonable limits. Trust but verify.
|
|
8
8
|
|
|
9
|
-
## Quick Deploy
|
|
10
|
-
|
|
11
|
-
[](https://railway.com/deploy/tezosx-mcp?referralCode=SVg46H&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
|
12
|
-
<details>
|
|
13
|
-
<summary>Railway deployment steps</summary>
|
|
14
|
-
|
|
15
|
-
1. Deploy the template (set `TEZOS_NETWORK` to `shadownet` before clicking deploy if desired)
|
|
16
|
-
2. Click the deployed item and go to "Settings"
|
|
17
|
-
3. Scroll down to "Public Networking"
|
|
18
|
-
4. Your domain will be something like `tezosx-mcp-production-a12b.up.railway.app`
|
|
19
|
-
5. Navigate to your domain to open the frontend config, and set up your spending key and contract address
|
|
20
|
-
6. Back on Railway, navigate to the "Variables" tab and set `SPENDING_PRIVATE_KEY` and `SPENDING_CONTRACT` to the values you received
|
|
21
|
-
7. Optional: Enable the 'serverless' setting to reduce resource usage
|
|
22
|
-
8. Restart the deployment
|
|
23
|
-
9. Set up your AI Platform to use `[your domain]/mcp` as the URL
|
|
24
|
-
|
|
25
|
-
</details>
|
|
26
|
-
|
|
27
|
-
[](https://render.com/deploy?repo=https://github.com/ecadlabs/TezosX-mcp)
|
|
28
|
-
<details>
|
|
29
|
-
<summary>Render deployment steps</summary>
|
|
30
|
-
|
|
31
|
-
1. Click the button above to deploy the template
|
|
32
|
-
2. Once deployed, under "Sync" click "View details"
|
|
33
|
-
3. Click the hyperlink to "tezosx-mcp"
|
|
34
|
-
4. Navigate to your onrender.com custom URL and set up your spending key and contract address
|
|
35
|
-
5. Back on Render, navigate to the "Environment" tab and set `SPENDING_PRIVATE_KEY` and `SPENDING_CONTRACT` environment variables
|
|
36
|
-
6. Click "Manual deploy" at the top right and select "Restart service"
|
|
37
|
-
7. Set up your AI Platform to use `[your domain]/mcp` as the URL
|
|
38
|
-
|
|
39
|
-
Note: Render spins down free plan services during inactivity. The next request can take up to a minute while the instance spins back up. Upgrade to a paid plan to avoid this.
|
|
40
|
-
|
|
41
|
-
</details>
|
|
42
|
-
|
|
43
9
|
## Components
|
|
44
10
|
|
|
45
11
|
| Component | Description | Deployment |
|
|
@@ -55,37 +21,95 @@ Note: Render spins down free plan services during inactivity. The next request c
|
|
|
55
21
|
|
|
56
22
|
### Installation
|
|
57
23
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```
|
|
24
|
+
<details>
|
|
25
|
+
<summary><strong>Local (npx)</strong> — fastest</summary>
|
|
61
26
|
|
|
62
|
-
|
|
27
|
+
The quickest path. Run the MCP locally alongside Claude Desktop — a built-in dashboard handles all configuration automatically.
|
|
63
28
|
|
|
64
|
-
|
|
65
|
-
npx tezosx-mcp
|
|
66
|
-
```
|
|
29
|
+
1. **Add to your Claude Desktop config** (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
67
30
|
|
|
68
|
-
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"tezos": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "@ecadlabs/tezosx-mcp"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
69
41
|
|
|
70
|
-
|
|
42
|
+
2. **Restart Claude Desktop.** Open your dashboard at `localhost:13205`, or ask Claude for the link.
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"tezos": {
|
|
76
|
-
"command": "npx",
|
|
77
|
-
"args": ["-y", "tezosx-mcp"],
|
|
78
|
-
"env": {
|
|
79
|
-
"TEZOS_NETWORK": "mainnet",
|
|
80
|
-
"SPENDING_CONTRACT": "KT1...",
|
|
81
|
-
"SPENDING_PRIVATE_KEY": "edsk..."
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
```
|
|
44
|
+
3. **Deploy your spending contract.** Connect your wallet, deploy the spending contract, and set your spending limits. Everything else is handled for you.
|
|
45
|
+
|
|
46
|
+
</details>
|
|
87
47
|
|
|
88
|
-
|
|
48
|
+
<details>
|
|
49
|
+
<summary><strong>Self-Hosted</strong> — always on</summary>
|
|
50
|
+
|
|
51
|
+
Deploy on Railway or Render for a remote MCP that's still entirely under your control and accessible from multiple clients. Requires some manual configuration.
|
|
52
|
+
|
|
53
|
+
1. **One-click deploy:**
|
|
54
|
+
|
|
55
|
+
[](https://railway.com/deploy/tezosx-mcp?referralCode=SVg46H&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
|
56
|
+
|
|
57
|
+
[](https://render.com/deploy?repo=https://github.com/ecadlabs/TezosX-mcp)
|
|
58
|
+
|
|
59
|
+
2. **Open dashboard & deploy contract.** Visit your deployment URL, connect your wallet, deploy the spending contract, and set your spending limits. Copy the provided spending key and contract address.
|
|
60
|
+
|
|
61
|
+
3. **Set environment variables on your server:**
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
SPENDING_PRIVATE_KEY=edsk...
|
|
65
|
+
SPENDING_CONTRACT=KT1...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
4. **Point Claude at your MCP:**
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"tezos": {
|
|
74
|
+
"type": "streamable-http",
|
|
75
|
+
"url": "https://your-mcp-url.example.com"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary><strong>Hosted Dashboard</strong> — flexible</summary>
|
|
85
|
+
|
|
86
|
+
Use our hosted dashboard to deploy your contract and generate keys, but run the MCP server locally. Keys never leave your browser.
|
|
87
|
+
|
|
88
|
+
1. **Open the [hosted dashboard](https://7687adbb.tezosx-dashboard.pages.dev/).**
|
|
89
|
+
|
|
90
|
+
2. **Deploy contract & copy credentials.** Connect your wallet, deploy the spending contract, and set your spending limits. Copy the provided spending key and contract address.
|
|
91
|
+
|
|
92
|
+
3. **Add your copied variables to your Claude config:**
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"mcpServers": {
|
|
97
|
+
"tezos": {
|
|
98
|
+
"command": "npx",
|
|
99
|
+
"args": ["-y", "@ecadlabs/tezosx-mcp"],
|
|
100
|
+
"env": {
|
|
101
|
+
"CONTRACT_ADDRESS": "KT1...",
|
|
102
|
+
"SPENDING_PRIVATE_KEY": "edsk...",
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
<details>
|
|
112
|
+
<summary><strong>From source</strong></summary>
|
|
89
113
|
|
|
90
114
|
1. Clone the TezosX-mcp repository
|
|
91
115
|
2. In the `/TezosX-mcp/mcp` folder run `npm i && npm run build`
|
|
@@ -95,17 +119,14 @@ Or run from source:
|
|
|
95
119
|
"mcpServers": {
|
|
96
120
|
"tezosx": {
|
|
97
121
|
"command": "node",
|
|
98
|
-
"args": ["/path/to/TezosX-mcp/mcp/dist/index.js"]
|
|
99
|
-
"env": {
|
|
100
|
-
"TEZOS_NETWORK": "mainnet",
|
|
101
|
-
"SPENDING_CONTRACT": "KT1...",
|
|
102
|
-
"SPENDING_PRIVATE_KEY": "edsk..."
|
|
103
|
-
}
|
|
122
|
+
"args": ["/path/to/TezosX-mcp/mcp/dist/index.js"]
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
}
|
|
107
126
|
```
|
|
108
127
|
|
|
128
|
+
</details>
|
|
129
|
+
|
|
109
130
|
### Environment Variables
|
|
110
131
|
|
|
111
132
|
| Variable | Required | Description |
|
package/dist/api.d.ts
ADDED
package/dist/api.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { InMemorySigner } from '@taquito/signer';
|
|
3
|
+
import { b58cencode, prefix } from '@taquito/utils';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
import { configureLiveConfig, resetLiveConfig, NETWORKS } from './live-config.js';
|
|
6
|
+
import { savePendingKey, loadPendingKey, activatePendingKey, saveContract, clearConfig, loadConfig } from './config-store.js';
|
|
7
|
+
const log = (msg) => console.error(`[tezosx-mcp] ${msg}`);
|
|
8
|
+
const LOCALHOST_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
9
|
+
/** Reject requests that aren't from localhost (prevents DNS rebinding and CSRF). */
|
|
10
|
+
function localhostGuard(req, res, next) {
|
|
11
|
+
const host = (req.hostname || '').toLowerCase();
|
|
12
|
+
if (!LOCALHOST_HOSTS.has(host)) {
|
|
13
|
+
log(`Blocked request from non-localhost host: ${host}`);
|
|
14
|
+
res.status(403).json({ error: 'Forbidden: API is localhost-only' });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// CSRF check: if an Origin header is present, it must also be localhost
|
|
18
|
+
const origin = req.headers.origin;
|
|
19
|
+
if (origin) {
|
|
20
|
+
try {
|
|
21
|
+
const originHost = new URL(origin).hostname.toLowerCase();
|
|
22
|
+
if (!LOCALHOST_HOSTS.has(originHost)) {
|
|
23
|
+
log(`Blocked request with non-localhost origin: ${origin}`);
|
|
24
|
+
res.status(403).json({ error: 'Forbidden: cross-origin requests not allowed' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
res.status(403).json({ error: 'Forbidden: malformed Origin header' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
next();
|
|
34
|
+
}
|
|
35
|
+
async function generateKeypair() {
|
|
36
|
+
const seed = randomBytes(32);
|
|
37
|
+
const secretKey = b58cencode(seed, prefix.edsk2);
|
|
38
|
+
const signer = await InMemorySigner.fromSecretKey(secretKey);
|
|
39
|
+
const publicKey = await signer.publicKey();
|
|
40
|
+
const address = await signer.publicKeyHash();
|
|
41
|
+
return { address, publicKey, secretKey };
|
|
42
|
+
}
|
|
43
|
+
export function createApiRouter(liveConfig) {
|
|
44
|
+
const router = Router();
|
|
45
|
+
// All API routes are localhost-only
|
|
46
|
+
router.use('/api', localhostGuard);
|
|
47
|
+
// Check config status (never exposes private key or filesystem paths)
|
|
48
|
+
router.get('/api/status', (_req, res) => {
|
|
49
|
+
res.json({
|
|
50
|
+
configured: liveConfig.configured,
|
|
51
|
+
spenderAddress: liveConfig.spendingAddress || undefined,
|
|
52
|
+
contractAddress: liveConfig.spendingContract || undefined,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// Generate keypair server-side, persist as *pending* key, return only public info.
|
|
56
|
+
// Does NOT hot-reload the signer — the old key stays active until /api/activate-key
|
|
57
|
+
// or /api/save-contract promotes the pending key.
|
|
58
|
+
// Idempotent: if a pending key already exists, returns its public info instead of
|
|
59
|
+
// generating a new one (prevents accidental overwrites if the on-chain tx fails).
|
|
60
|
+
router.post('/api/generate-keypair', async (_req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
// Return existing pending key if one hasn't been activated yet
|
|
63
|
+
const existingPending = loadPendingKey();
|
|
64
|
+
if (existingPending) {
|
|
65
|
+
const signer = await InMemorySigner.fromSecretKey(existingPending);
|
|
66
|
+
const publicKey = await signer.publicKey();
|
|
67
|
+
const address = await signer.publicKeyHash();
|
|
68
|
+
log(`Returning existing pending keypair: ${address}`);
|
|
69
|
+
res.json({ address, publicKey });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const keypair = await generateKeypair();
|
|
73
|
+
// Save to pending slot (active key stays untouched)
|
|
74
|
+
savePendingKey(keypair.secretKey);
|
|
75
|
+
log(`Generated and saved new pending keypair: ${keypair.address}`);
|
|
76
|
+
// Return only public info — private key stays on server
|
|
77
|
+
res.json({
|
|
78
|
+
address: keypair.address,
|
|
79
|
+
publicKey: keypair.publicKey,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
log(`Failed to generate keypair: ${error instanceof Error ? error.message : error}`);
|
|
84
|
+
res.status(500).json({ error: 'Failed to generate keypair' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// Activate the pending key — call this after on-chain setSpender confirms.
|
|
88
|
+
// Promotes pendingPrivateKey → spendingPrivateKey and hot-reloads the signer.
|
|
89
|
+
router.post('/api/activate-key', async (_req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const pendingKey = loadPendingKey();
|
|
92
|
+
if (!pendingKey) {
|
|
93
|
+
res.status(400).json({ error: 'No pending key found. Generate a keypair first.' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!liveConfig.spendingContract) {
|
|
97
|
+
res.status(400).json({ error: 'No contract configured. Save a contract first.' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Promote pending → active in the store
|
|
101
|
+
activatePendingKey();
|
|
102
|
+
const spenderAddress = await configureLiveConfig(liveConfig, pendingKey, liveConfig.spendingContract);
|
|
103
|
+
log(`LiveConfig signer activated: ${spenderAddress}`);
|
|
104
|
+
res.json({ success: true, spenderAddress });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
log(`Failed to activate key: ${error instanceof Error ? error.message : error}`);
|
|
108
|
+
res.status(500).json({ error: 'Failed to activate key' });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// Save contract address + network, complete configuration
|
|
112
|
+
router.post('/api/save-contract', async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const { contractAddress, network } = req.body;
|
|
115
|
+
if (!contractAddress || !contractAddress.startsWith('KT1')) {
|
|
116
|
+
res.status(400).json({ error: 'Invalid contract address. Must start with KT1.' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const networkName = (network || 'mainnet');
|
|
120
|
+
if (!(networkName in NETWORKS)) {
|
|
121
|
+
res.status(400).json({ error: `Invalid network: ${networkName}` });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Save to persistent store
|
|
125
|
+
saveContract(contractAddress, networkName);
|
|
126
|
+
log(`Saved contract address: ${contractAddress} on ${networkName}`);
|
|
127
|
+
// Promote pending key → active (initial deploy flow)
|
|
128
|
+
const pendingKey = loadPendingKey();
|
|
129
|
+
const stored = loadConfig();
|
|
130
|
+
const privateKey = pendingKey || stored.spendingPrivateKey;
|
|
131
|
+
if (!privateKey) {
|
|
132
|
+
res.status(400).json({ error: 'No spending key found. Generate a keypair first.' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (pendingKey) {
|
|
136
|
+
activatePendingKey();
|
|
137
|
+
}
|
|
138
|
+
const spenderAddress = await configureLiveConfig(liveConfig, privateKey, contractAddress, networkName);
|
|
139
|
+
log(`LiveConfig updated: spender=${spenderAddress}, contract=${contractAddress}`);
|
|
140
|
+
res.json({ success: true, spenderAddress });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
log(`Failed to save contract: ${error instanceof Error ? error.message : error}`);
|
|
144
|
+
res.status(500).json({ error: 'Failed to save contract configuration' });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// Clear all stored config
|
|
148
|
+
router.delete('/api/config', (_req, res) => {
|
|
149
|
+
clearConfig();
|
|
150
|
+
resetLiveConfig(liveConfig);
|
|
151
|
+
log('Config cleared');
|
|
152
|
+
res.json({ success: true });
|
|
153
|
+
});
|
|
154
|
+
return router;
|
|
155
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface StoredConfig {
|
|
2
|
+
spendingPrivateKey: string;
|
|
3
|
+
pendingPrivateKey: string;
|
|
4
|
+
spendingContract: string;
|
|
5
|
+
network: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadConfig(): Partial<StoredConfig>;
|
|
8
|
+
export declare function savePendingKey(key: string): void;
|
|
9
|
+
export declare function loadPendingKey(): string | undefined;
|
|
10
|
+
/** Promote the pending key to active and clear the pending slot. */
|
|
11
|
+
export declare function activatePendingKey(): string;
|
|
12
|
+
export declare function saveContract(address: string, network: string): void;
|
|
13
|
+
export declare function clearConfig(): void;
|
|
14
|
+
export declare function getStorePath(): string;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
const store = new Conf({
|
|
3
|
+
projectName: 'tezosx-mcp',
|
|
4
|
+
configFileMode: 0o600,
|
|
5
|
+
});
|
|
6
|
+
export function loadConfig() {
|
|
7
|
+
const spendingPrivateKey = store.get('spendingPrivateKey');
|
|
8
|
+
const spendingContract = store.get('spendingContract');
|
|
9
|
+
const network = store.get('network');
|
|
10
|
+
if (!spendingPrivateKey && !spendingContract && !network)
|
|
11
|
+
return {};
|
|
12
|
+
return { spendingPrivateKey, spendingContract, network };
|
|
13
|
+
}
|
|
14
|
+
export function savePendingKey(key) {
|
|
15
|
+
store.set('pendingPrivateKey', key);
|
|
16
|
+
}
|
|
17
|
+
export function loadPendingKey() {
|
|
18
|
+
return store.get('pendingPrivateKey') || undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Promote the pending key to active and clear the pending slot. */
|
|
21
|
+
export function activatePendingKey() {
|
|
22
|
+
const key = store.get('pendingPrivateKey');
|
|
23
|
+
if (!key)
|
|
24
|
+
throw new Error('No pending key to activate');
|
|
25
|
+
store.set('spendingPrivateKey', key);
|
|
26
|
+
store.delete('pendingPrivateKey');
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
export function saveContract(address, network) {
|
|
30
|
+
store.set('spendingContract', address);
|
|
31
|
+
store.set('network', network);
|
|
32
|
+
}
|
|
33
|
+
export function clearConfig() {
|
|
34
|
+
store.clear();
|
|
35
|
+
}
|
|
36
|
+
export function getStorePath() {
|
|
37
|
+
return store.path;
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export const DEFAULT_WEB_PORT = '13205';
|
|
3
3
|
import { config } from 'dotenv';
|
|
4
|
-
// Taquito
|
|
5
|
-
import { InMemorySigner } from "@taquito/signer";
|
|
6
|
-
import { TezosToolkit } from "@taquito/taquito";
|
|
7
4
|
// MCP
|
|
8
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -15,6 +12,10 @@ import { startWebServer } from "./webserver.js";
|
|
|
15
12
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16
13
|
import { fileURLToPath } from "url";
|
|
17
14
|
import { join } from "path";
|
|
15
|
+
// Config
|
|
16
|
+
import { createLiveConfig, configureLiveConfig, NETWORKS } from "./live-config.js";
|
|
17
|
+
import { loadConfig } from "./config-store.js";
|
|
18
|
+
import { createApiRouter } from "./api.js";
|
|
18
19
|
config({ quiet: true });
|
|
19
20
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
20
21
|
// Global error handlers
|
|
@@ -24,57 +25,40 @@ process.on('uncaughtException', (err) => {
|
|
|
24
25
|
process.on('unhandledRejection', (reason) => {
|
|
25
26
|
console.error('[tezosx-mcp] Unhandled rejection:', reason);
|
|
26
27
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
rpcUrl: 'https://mainnet.tezos.ecadinfra.com',
|
|
31
|
-
tzktApi: 'https://api.tzkt.io',
|
|
32
|
-
},
|
|
33
|
-
shadownet: {
|
|
34
|
-
rpcUrl: 'https://shadownet.tezos.ecadinfra.com',
|
|
35
|
-
tzktApi: 'https://api.shadownet.tzkt.io',
|
|
36
|
-
},
|
|
37
|
-
};
|
|
28
|
+
function isValidPrivateKey(key) {
|
|
29
|
+
return key.startsWith('edsk') || key.startsWith('spsk') || key.startsWith('p2sk');
|
|
30
|
+
}
|
|
38
31
|
const log = (msg) => console.error(`[tezosx-mcp] ${msg}`);
|
|
39
32
|
const init = async () => {
|
|
40
33
|
log('Starting server...');
|
|
41
|
-
//
|
|
42
|
-
const skipFrontend = process.env.SKIP_FRONTEND === 'true' || process.env.MCP_TRANSPORT === 'http';
|
|
43
|
-
if (!skipFrontend) {
|
|
44
|
-
const webPort = parseInt(process.env.WEB_PORT || DEFAULT_WEB_PORT, 10);
|
|
45
|
-
startWebServer(webPort);
|
|
46
|
-
log(`Frontend server started on port ${webPort}`);
|
|
47
|
-
}
|
|
48
|
-
const server = new McpServer({
|
|
49
|
-
name: "tezosx-mcp",
|
|
50
|
-
version: "1.0.0"
|
|
51
|
-
});
|
|
52
|
-
// Network configuration
|
|
34
|
+
// Determine network
|
|
53
35
|
const networkName = (process.env.TEZOS_NETWORK || 'mainnet');
|
|
54
|
-
|
|
55
|
-
if (!network) {
|
|
36
|
+
if (!(networkName in NETWORKS)) {
|
|
56
37
|
throw new ReferenceError(`Invalid network: ${networkName}. Valid options: ${Object.keys(NETWORKS).join(', ')}`);
|
|
57
38
|
}
|
|
58
39
|
log(`Network: ${networkName}`);
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const privateKey = process.env.SPENDING_PRIVATE_KEY?.trim();
|
|
64
|
-
const spendingContract = process.env.SPENDING_CONTRACT?.trim();
|
|
40
|
+
// Create shared mutable config
|
|
41
|
+
const liveConfig = createLiveConfig(networkName);
|
|
42
|
+
// Try to load config: persistent store first, then env vars
|
|
43
|
+
const stored = loadConfig();
|
|
44
|
+
const privateKey = stored.spendingPrivateKey?.trim() || process.env.SPENDING_PRIVATE_KEY?.trim();
|
|
45
|
+
const spendingContract = stored.spendingContract?.trim() || process.env.SPENDING_CONTRACT?.trim();
|
|
46
|
+
const storedNetwork = stored.network && stored.network in NETWORKS ? stored.network : undefined;
|
|
47
|
+
const configNetwork = storedNetwork || networkName;
|
|
65
48
|
if (privateKey && spendingContract) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!privateKey.startsWith('edsk') && !privateKey.startsWith('spsk') && !privateKey.startsWith('p2sk')) {
|
|
69
|
-
log(`Warning: Invalid SPENDING_PRIVATE_KEY format. Must start with edsk, spsk, or p2sk. Wallet not configured.`);
|
|
49
|
+
if (!isValidPrivateKey(privateKey)) {
|
|
50
|
+
log('Warning: Invalid private key format. Must start with edsk, spsk, or p2sk. Wallet not configured.');
|
|
70
51
|
}
|
|
71
52
|
else {
|
|
72
53
|
try {
|
|
73
|
-
const
|
|
74
|
-
Tezos.setSignerProvider(signer);
|
|
75
|
-
const spendingAddress = await Tezos.signer.publicKeyHash();
|
|
76
|
-
walletConfig = { Tezos, spendingContract, spendingAddress };
|
|
54
|
+
const spendingAddress = await configureLiveConfig(liveConfig, privateKey, spendingContract, configNetwork);
|
|
77
55
|
log(`Wallet configured: ${spendingAddress}`);
|
|
56
|
+
if (stored.spendingPrivateKey) {
|
|
57
|
+
log('Config loaded from persistent store');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
log('Config loaded from environment variables');
|
|
61
|
+
}
|
|
78
62
|
}
|
|
79
63
|
catch (error) {
|
|
80
64
|
log(`Warning: Failed to initialize signer: ${error instanceof Error ? error.message : 'Unknown error'}. Wallet not configured.`);
|
|
@@ -82,21 +66,36 @@ const init = async () => {
|
|
|
82
66
|
}
|
|
83
67
|
}
|
|
84
68
|
else {
|
|
85
|
-
log('Wallet not configured
|
|
69
|
+
log('Wallet not configured — visit the dashboard to set up');
|
|
86
70
|
}
|
|
71
|
+
const server = new McpServer({
|
|
72
|
+
name: "tezosx-mcp",
|
|
73
|
+
version: "1.0.0"
|
|
74
|
+
});
|
|
87
75
|
// Tools
|
|
88
76
|
log('Registering tools...');
|
|
89
77
|
const http = process.env.MCP_TRANSPORT === 'http';
|
|
90
|
-
const tools = createTools(
|
|
78
|
+
const tools = createTools(liveConfig, http);
|
|
91
79
|
tools.forEach(tool => {
|
|
92
80
|
server.registerTool(tool.name, tool.config, tool.handler);
|
|
93
81
|
});
|
|
94
82
|
log(`Registered ${tools.length} tools`);
|
|
83
|
+
// API router (shared between both transports)
|
|
84
|
+
const apiRouter = createApiRouter(liveConfig);
|
|
85
|
+
// Start web server for frontend (skip if SKIP_FRONTEND is set or if using HTTP transport)
|
|
86
|
+
const skipFrontend = process.env.SKIP_FRONTEND === 'true' || http;
|
|
87
|
+
if (!skipFrontend) {
|
|
88
|
+
const webPort = parseInt(process.env.WEB_PORT || DEFAULT_WEB_PORT, 10);
|
|
89
|
+
startWebServer(webPort, apiRouter);
|
|
90
|
+
log(`Frontend server started on port ${webPort}`);
|
|
91
|
+
}
|
|
95
92
|
const transport = process.env.MCP_TRANSPORT || 'stdio';
|
|
96
93
|
log(`Transport: ${transport}`);
|
|
97
94
|
if (transport === 'http') {
|
|
98
95
|
const app = express();
|
|
99
96
|
app.use(express.json());
|
|
97
|
+
// API routes
|
|
98
|
+
app.use(apiRouter);
|
|
100
99
|
// Dashboard frontend (serve from frontend/dist)
|
|
101
100
|
const frontendPath = join(__dirname, "../frontend/dist");
|
|
102
101
|
app.use(express.static(frontendPath));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TezosToolkit } from "@taquito/taquito";
|
|
2
|
+
export interface LiveConfig {
|
|
3
|
+
Tezos: TezosToolkit;
|
|
4
|
+
spendingContract: string;
|
|
5
|
+
spendingAddress: string;
|
|
6
|
+
tzktApi: string;
|
|
7
|
+
configured: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const NETWORKS: {
|
|
10
|
+
readonly mainnet: {
|
|
11
|
+
readonly rpcUrl: "https://mainnet.tezos.ecadinfra.com";
|
|
12
|
+
readonly tzktApi: "https://api.tzkt.io";
|
|
13
|
+
};
|
|
14
|
+
readonly shadownet: {
|
|
15
|
+
readonly rpcUrl: "https://shadownet.tezos.ecadinfra.com";
|
|
16
|
+
readonly tzktApi: "https://api.shadownet.tzkt.io";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type NetworkName = keyof typeof NETWORKS;
|
|
20
|
+
/**
|
|
21
|
+
* Creates an unconfigured LiveConfig pointed at the given network.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createLiveConfig(networkName: NetworkName): LiveConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Mutates the LiveConfig in-place with a new private key and contract address.
|
|
26
|
+
* Returns the derived spender address.
|
|
27
|
+
*/
|
|
28
|
+
export declare function configureLiveConfig(config: LiveConfig, privateKey: string, spendingContract: string, networkName?: NetworkName): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Resets the LiveConfig to unconfigured state.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resetLiveConfig(config: LiveConfig, networkName?: NetworkName): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { TezosToolkit } from "@taquito/taquito";
|
|
2
|
+
import { InMemorySigner } from "@taquito/signer";
|
|
3
|
+
export const NETWORKS = {
|
|
4
|
+
mainnet: {
|
|
5
|
+
rpcUrl: 'https://mainnet.tezos.ecadinfra.com',
|
|
6
|
+
tzktApi: 'https://api.tzkt.io',
|
|
7
|
+
},
|
|
8
|
+
shadownet: {
|
|
9
|
+
rpcUrl: 'https://shadownet.tezos.ecadinfra.com',
|
|
10
|
+
tzktApi: 'https://api.shadownet.tzkt.io',
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Creates an unconfigured LiveConfig pointed at the given network.
|
|
15
|
+
*/
|
|
16
|
+
export function createLiveConfig(networkName) {
|
|
17
|
+
const network = NETWORKS[networkName];
|
|
18
|
+
return {
|
|
19
|
+
Tezos: new TezosToolkit(network.rpcUrl),
|
|
20
|
+
spendingContract: '',
|
|
21
|
+
spendingAddress: '',
|
|
22
|
+
tzktApi: network.tzktApi,
|
|
23
|
+
configured: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Mutates the LiveConfig in-place with a new private key and contract address.
|
|
28
|
+
* Returns the derived spender address.
|
|
29
|
+
*/
|
|
30
|
+
export async function configureLiveConfig(config, privateKey, spendingContract, networkName) {
|
|
31
|
+
if (networkName) {
|
|
32
|
+
const network = NETWORKS[networkName];
|
|
33
|
+
config.Tezos = new TezosToolkit(network.rpcUrl);
|
|
34
|
+
config.tzktApi = network.tzktApi;
|
|
35
|
+
}
|
|
36
|
+
const signer = await InMemorySigner.fromSecretKey(privateKey);
|
|
37
|
+
config.Tezos.setSignerProvider(signer);
|
|
38
|
+
config.spendingAddress = await config.Tezos.signer.publicKeyHash();
|
|
39
|
+
config.spendingContract = spendingContract;
|
|
40
|
+
config.configured = true;
|
|
41
|
+
return config.spendingAddress;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resets the LiveConfig to unconfigured state.
|
|
45
|
+
*/
|
|
46
|
+
export function resetLiveConfig(config, networkName) {
|
|
47
|
+
const rpcUrl = networkName ? NETWORKS[networkName].rpcUrl : config.Tezos.rpc.getRpcUrl();
|
|
48
|
+
config.Tezos = new TezosToolkit(rpcUrl);
|
|
49
|
+
if (networkName) {
|
|
50
|
+
config.tzktApi = NETWORKS[networkName].tzktApi;
|
|
51
|
+
}
|
|
52
|
+
config.spendingContract = '';
|
|
53
|
+
config.spendingAddress = '';
|
|
54
|
+
config.configured = false;
|
|
55
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { TezosToolkit } from "@taquito/taquito";
|
|
2
1
|
import z from "zod";
|
|
3
|
-
|
|
2
|
+
import type { LiveConfig } from "../live-config.js";
|
|
3
|
+
export declare const createCreateX402PaymentTool: (config: LiveConfig) => {
|
|
4
4
|
name: string;
|
|
5
5
|
config: {
|
|
6
6
|
title: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { signX402Payment } from "./x402/sign.js";
|
|
3
|
-
export const createCreateX402PaymentTool = (
|
|
3
|
+
export const createCreateX402PaymentTool = (config) => ({
|
|
4
4
|
name: "tezos_create_x402_payment",
|
|
5
5
|
config: {
|
|
6
6
|
title: "Create x402 Payment",
|
|
@@ -19,6 +19,7 @@ export const createCreateX402PaymentTool = (Tezos) => ({
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
handler: async (params) => {
|
|
22
|
+
const { Tezos } = config;
|
|
22
23
|
const { network, asset, amount, recipient } = params;
|
|
23
24
|
// Validate asset type
|
|
24
25
|
if (asset !== "XTZ") {
|