@ecadlabs/tezosx-mcp 1.0.0 → 1.0.2

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.
Files changed (52) hide show
  1. package/README.md +87 -66
  2. package/dist/api.d.ts +3 -0
  3. package/dist/api.js +155 -0
  4. package/dist/config-store.d.ts +15 -0
  5. package/dist/config-store.js +38 -0
  6. package/dist/index.d.ts +0 -6
  7. package/dist/index.js +43 -44
  8. package/dist/live-config.d.ts +32 -0
  9. package/dist/live-config.js +55 -0
  10. package/dist/tools/create_x402_payment.d.ts +2 -2
  11. package/dist/tools/create_x402_payment.js +2 -1
  12. package/dist/tools/fetch_with_x402.d.ts +3 -3
  13. package/dist/tools/fetch_with_x402.js +7 -6
  14. package/dist/tools/get_addresses.d.ts +2 -2
  15. package/dist/tools/get_addresses.js +2 -1
  16. package/dist/tools/get_balance.d.ts +2 -2
  17. package/dist/tools/get_balance.js +2 -1
  18. package/dist/tools/get_dashboard.d.ts +2 -1
  19. package/dist/tools/get_dashboard.js +2 -2
  20. package/dist/tools/get_limits.d.ts +2 -2
  21. package/dist/tools/get_limits.js +2 -1
  22. package/dist/tools/get_operation_history.d.ts +2 -1
  23. package/dist/tools/get_operation_history.js +2 -1
  24. package/dist/tools/index.d.ts +3 -3
  25. package/dist/tools/index.js +15 -24
  26. package/dist/tools/reveal_account.d.ts +2 -1
  27. package/dist/tools/reveal_account.js +2 -1
  28. package/dist/tools/send_xtz.d.ts +2 -9
  29. package/dist/tools/send_xtz.js +33 -15
  30. package/dist/webserver.d.ts +2 -1
  31. package/dist/webserver.js +11 -4
  32. package/frontend/dist/assets/index-CTdz8_ps.css +1 -0
  33. package/frontend/dist/assets/index-Do1wIlnj.js +278 -0
  34. package/frontend/dist/index.html +2 -2
  35. package/package.json +5 -3
  36. package/dist/adapters/index.d.ts +0 -37
  37. package/dist/adapters/index.js +0 -57
  38. package/dist/adapters/node.d.ts +0 -18
  39. package/dist/adapters/node.js +0 -35
  40. package/dist/adapters/types.d.ts +0 -52
  41. package/dist/adapters/types.js +0 -25
  42. package/dist/adapters/worker.d.ts +0 -35
  43. package/dist/adapters/worker.js +0 -50
  44. package/dist/server.d.ts +0 -36
  45. package/dist/server.js +0 -80
  46. package/dist/tools/get_address.d.ts +0 -20
  47. package/dist/tools/get_address.js +0 -24
  48. package/dist/worker.bundle.js +0 -134265
  49. package/dist/worker.d.ts +0 -13
  50. package/dist/worker.js +0 -132
  51. package/frontend/dist/assets/index-RtTL1nIl.js +0 -257
  52. 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 alpha. 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).
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
- [![Deploy on Railway](https://railway.com/button.svg)](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
- [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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
- ```bash
59
- npm install @ecadlabs/tezosx-mcp
60
- ```
24
+ <details>
25
+ <summary><strong>Local (npx)</strong> — fastest</summary>
61
26
 
62
- Or run directly:
27
+ The quickest path. Run the MCP locally alongside Claude Desktop — a built-in dashboard handles all configuration automatically.
63
28
 
64
- ```bash
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
- ### Claude Desktop Configuration
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "tezos": {
35
+ "command": "npx",
36
+ "args": ["-y", "@ecadlabs/tezosx-mcp"]
37
+ }
38
+ }
39
+ }
40
+ ```
69
41
 
70
- Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
42
+ 2. **Restart Claude Desktop.** Open your dashboard at `localhost:13205`, or ask Claude for the link.
71
43
 
72
- ```json
73
- {
74
- "mcpServers": {
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
- Or run from source:
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
+ [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/tezosx-mcp?referralCode=SVg46H&utm_medium=integration&utm_source=template&utm_campaign=generic)
56
+
57
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](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
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import { LiveConfig } from './live-config.js';
3
+ export declare function createApiRouter(liveConfig: LiveConfig): Router;
package/dist/api.js ADDED
@@ -0,0 +1,155 @@
1
+ import { Router } from 'express';
2
+ import { InMemorySigner } from '@taquito/signer';
3
+ import { b58Encode, PrefixV2 } 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 = b58Encode(seed, PrefixV2.Ed25519Seed);
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
@@ -1,8 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export declare const DEFAULT_WEB_PORT = "13205";
3
- import { TezosToolkit } from "@taquito/taquito";
4
- export type WalletConfig = {
5
- Tezos: TezosToolkit;
6
- spendingContract: string;
7
- spendingAddress: string;
8
- } | null;
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
- // Network configurations
28
- const NETWORKS = {
29
- mainnet: {
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
- // Start web server for frontend (skip if SKIP_FRONTEND is set or if using HTTP transport)
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
- const network = NETWORKS[networkName];
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
- // Taquito setup
60
- const Tezos = new TezosToolkit(network.rpcUrl);
61
- // Wallet configuration (optional - tools will guide user to configure if not set)
62
- let walletConfig = null;
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
- log('Configuring wallet...');
67
- // Validate private key format
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 signer = await InMemorySigner.fromSecretKey(privateKey);
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 (missing SPENDING_PRIVATE_KEY or SPENDING_CONTRACT)');
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(walletConfig, network.tzktApi, http);
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
- export declare const createCreateX402PaymentTool: (Tezos: TezosToolkit) => {
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 = (Tezos) => ({
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") {