@ecadlabs/tezosx-mcp 1.0.3 → 1.0.4
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/dist/api.js +1 -0
- package/dist/live-config.d.ts +1 -0
- package/dist/live-config.js +3 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/recover_spender_funds.d.ts +24 -0
- package/dist/tools/recover_spender_funds.js +113 -0
- package/dist/webserver.js +17 -7
- package/frontend/dist/assets/{index-BM2KDhgo.js → index-Cp61TXaW.js} +44 -44
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -50,6 +50,7 @@ export function createApiRouter(liveConfig) {
|
|
|
50
50
|
configured: liveConfig.configured,
|
|
51
51
|
spenderAddress: liveConfig.spendingAddress || undefined,
|
|
52
52
|
contractAddress: liveConfig.spendingContract || undefined,
|
|
53
|
+
network: liveConfig.networkName,
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
// Generate keypair server-side, persist as *pending* key, return only public info.
|
package/dist/live-config.d.ts
CHANGED
package/dist/live-config.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createLiveConfig(networkName) {
|
|
|
34
34
|
spendingContract: '',
|
|
35
35
|
spendingAddress: '',
|
|
36
36
|
tzktApi: network.tzktApi,
|
|
37
|
+
networkName,
|
|
37
38
|
configured: false,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
@@ -46,6 +47,7 @@ export async function configureLiveConfig(config, privateKey, spendingContract,
|
|
|
46
47
|
const network = NETWORKS[networkName];
|
|
47
48
|
config.Tezos = new TezosToolkit(network.rpcUrl);
|
|
48
49
|
config.tzktApi = network.tzktApi;
|
|
50
|
+
config.networkName = networkName;
|
|
49
51
|
}
|
|
50
52
|
const signer = await InMemorySigner.fromSecretKey(privateKey);
|
|
51
53
|
config.Tezos.setSignerProvider(signer);
|
|
@@ -62,6 +64,7 @@ export function resetLiveConfig(config, networkName) {
|
|
|
62
64
|
config.Tezos = new TezosToolkit(rpcUrl);
|
|
63
65
|
if (networkName) {
|
|
64
66
|
config.tzktApi = NETWORKS[networkName].tzktApi;
|
|
67
|
+
config.networkName = networkName;
|
|
65
68
|
}
|
|
66
69
|
config.spendingContract = '';
|
|
67
70
|
config.spendingAddress = '';
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -88,6 +88,27 @@ export declare const createTools: (liveConfig: LiveConfig, http: boolean) => ({
|
|
|
88
88
|
text: string;
|
|
89
89
|
}[];
|
|
90
90
|
}>;
|
|
91
|
+
} | {
|
|
92
|
+
name: string;
|
|
93
|
+
config: {
|
|
94
|
+
title: string;
|
|
95
|
+
description: string;
|
|
96
|
+
inputSchema: import("zod").ZodObject<{
|
|
97
|
+
destination: import("zod").ZodOptional<import("zod").ZodString>;
|
|
98
|
+
}, import("zod/v4/core").$strip>;
|
|
99
|
+
annotations: {
|
|
100
|
+
readOnlyHint: boolean;
|
|
101
|
+
destructiveHint: boolean;
|
|
102
|
+
idempotentHint: boolean;
|
|
103
|
+
openWorldHint: boolean;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
handler: (params: any) => Promise<{
|
|
107
|
+
content: {
|
|
108
|
+
type: "text";
|
|
109
|
+
text: string;
|
|
110
|
+
}[];
|
|
111
|
+
}>;
|
|
91
112
|
} | {
|
|
92
113
|
name: string;
|
|
93
114
|
config: {
|
package/dist/tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createGetLimitsTool } from "./get_limits.js";
|
|
|
6
6
|
import { createGetOperationHistoryTool } from "./get_operation_history.js";
|
|
7
7
|
import { createParseX402RequirementsTool } from "./parse_x402_requirements.js";
|
|
8
8
|
import { createRevealAccountTool } from "./reveal_account.js";
|
|
9
|
+
import { createRecoverSpenderFundsTool } from "./recover_spender_funds.js";
|
|
9
10
|
import { createSendXtzTool } from "./send_xtz.js";
|
|
10
11
|
import { createGetDashboardTool } from "./get_dashboard.js";
|
|
11
12
|
const getNotConfiguredMessage = () => `Wallet not configured.
|
|
@@ -39,6 +40,7 @@ export const createTools = (liveConfig, http) => {
|
|
|
39
40
|
createGetLimitsTool(liveConfig),
|
|
40
41
|
createGetOperationHistoryTool(liveConfig),
|
|
41
42
|
createParseX402RequirementsTool(),
|
|
43
|
+
createRecoverSpenderFundsTool(liveConfig),
|
|
42
44
|
createRevealAccountTool(liveConfig),
|
|
43
45
|
createSendXtzTool(liveConfig),
|
|
44
46
|
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type { LiveConfig } from "../live-config.js";
|
|
3
|
+
export declare const createRecoverSpenderFundsTool: (config: LiveConfig) => {
|
|
4
|
+
name: string;
|
|
5
|
+
config: {
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
inputSchema: z.ZodObject<{
|
|
9
|
+
destination: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, z.z.core.$strip>;
|
|
11
|
+
annotations: {
|
|
12
|
+
readOnlyHint: boolean;
|
|
13
|
+
destructiveHint: boolean;
|
|
14
|
+
idempotentHint: boolean;
|
|
15
|
+
openWorldHint: boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
handler: (params: any) => Promise<{
|
|
19
|
+
content: {
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { ensureRevealed } from "./reveal_account.js";
|
|
3
|
+
const CONFIRMATIONS_TO_WAIT = 3;
|
|
4
|
+
const inputSchema = z.object({
|
|
5
|
+
destination: z.string().optional().describe("Address to send recovered funds to. Defaults to the contract owner's address."),
|
|
6
|
+
});
|
|
7
|
+
export const createRecoverSpenderFundsTool = (config) => ({
|
|
8
|
+
name: "tezos_recover_spender_funds",
|
|
9
|
+
config: {
|
|
10
|
+
title: "Recover Spender Gas Funds",
|
|
11
|
+
description: "Transfers the spender address's XTZ balance (used for gas fees) to the contract owner or a specified address. " +
|
|
12
|
+
"Useful for recovering funds when decommissioning a spender or rebalancing. " +
|
|
13
|
+
"Always fetches the live on-chain balance — call this tool every time the user asks, even if a previous call returned zero.",
|
|
14
|
+
inputSchema,
|
|
15
|
+
annotations: {
|
|
16
|
+
readOnlyHint: false,
|
|
17
|
+
destructiveHint: true,
|
|
18
|
+
idempotentHint: false,
|
|
19
|
+
openWorldHint: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
handler: async (params) => {
|
|
23
|
+
params = params;
|
|
24
|
+
const { Tezos, spendingContract, spendingAddress, tzktApi } = config;
|
|
25
|
+
// Resolve destination: explicit param > contract owner > spending contract
|
|
26
|
+
let destination = params.destination;
|
|
27
|
+
if (!destination) {
|
|
28
|
+
if (spendingContract) {
|
|
29
|
+
const contract = await Tezos.contract.at(spendingContract);
|
|
30
|
+
const storage = await contract.storage();
|
|
31
|
+
destination = storage.owner;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!destination) {
|
|
35
|
+
throw new Error("No destination provided and no spending contract configured.");
|
|
36
|
+
}
|
|
37
|
+
const balance = await Tezos.tz.getBalance(spendingAddress);
|
|
38
|
+
const balanceMutez = balance.toNumber();
|
|
39
|
+
if (balanceMutez === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `Spender address (${spendingAddress}) has zero balance. Nothing to recover.`,
|
|
44
|
+
}],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
await ensureRevealed(Tezos);
|
|
48
|
+
// Drain account pattern (Taquito docs):
|
|
49
|
+
// For KT1 destinations, call the default entrypoint explicitly.
|
|
50
|
+
// For implicit accounts, use a simple transfer.
|
|
51
|
+
const isContract = destination.startsWith("KT1");
|
|
52
|
+
if (isContract) {
|
|
53
|
+
const contract = await Tezos.contract.at(destination);
|
|
54
|
+
const depositCall = contract.methodsObject.default_(null);
|
|
55
|
+
const estimate = await Tezos.estimate.contractCall(depositCall);
|
|
56
|
+
const maxAmount = balanceMutez - estimate.suggestedFeeMutez;
|
|
57
|
+
if (maxAmount <= 0) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Spender balance (${balanceMutez} mutez) is too low to cover transfer fees (${estimate.suggestedFeeMutez} mutez). Nothing to recover.`,
|
|
62
|
+
}],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const operation = await contract.methodsObject.default_(null).send({
|
|
66
|
+
amount: maxAmount,
|
|
67
|
+
mutez: true,
|
|
68
|
+
fee: estimate.suggestedFeeMutez,
|
|
69
|
+
gasLimit: estimate.gasLimit,
|
|
70
|
+
storageLimit: 0,
|
|
71
|
+
});
|
|
72
|
+
await operation.confirmation(CONFIRMATIONS_TO_WAIT);
|
|
73
|
+
const tzktBase = tzktApi.replace("api.", "");
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: `Recovered ${(maxAmount / 1_000_000).toFixed(6)} XTZ from spender (${spendingAddress}) to ${destination}.\n${tzktBase}/${operation.hash}`,
|
|
78
|
+
}],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Implicit account (tz1/tz2/tz3)
|
|
82
|
+
const estimate = await Tezos.estimate.transfer({
|
|
83
|
+
to: destination,
|
|
84
|
+
amount: balanceMutez,
|
|
85
|
+
mutez: true,
|
|
86
|
+
});
|
|
87
|
+
const maxAmount = balanceMutez - estimate.suggestedFeeMutez;
|
|
88
|
+
if (maxAmount <= 0) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: `Spender balance (${balanceMutez} mutez) is too low to cover transfer fees (${estimate.suggestedFeeMutez} mutez). Nothing to recover.`,
|
|
93
|
+
}],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const operation = await Tezos.contract.transfer({
|
|
97
|
+
to: destination,
|
|
98
|
+
amount: maxAmount,
|
|
99
|
+
mutez: true,
|
|
100
|
+
fee: estimate.suggestedFeeMutez,
|
|
101
|
+
gasLimit: estimate.gasLimit,
|
|
102
|
+
storageLimit: 0,
|
|
103
|
+
});
|
|
104
|
+
await operation.confirmation(CONFIRMATIONS_TO_WAIT);
|
|
105
|
+
const tzktBase = tzktApi.replace("api.", "");
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
type: "text",
|
|
109
|
+
text: `Recovered ${(maxAmount / 1_000_000).toFixed(6)} XTZ from spender (${spendingAddress}) to ${destination}.\n${tzktBase}/${operation.hash}`,
|
|
110
|
+
}],
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
});
|
package/dist/webserver.js
CHANGED
|
@@ -14,28 +14,38 @@ export const startWebServer = (port, apiRouter) => {
|
|
|
14
14
|
const distPath = join(__dirname, "../frontend/dist");
|
|
15
15
|
app.use(sirv(distPath, { single: true }));
|
|
16
16
|
let retries = 0;
|
|
17
|
-
const maxRetries =
|
|
17
|
+
const maxRetries = 5;
|
|
18
|
+
const retryDelay = 1000;
|
|
19
|
+
let activeServer = null;
|
|
18
20
|
const tryListen = () => {
|
|
19
21
|
const server = app.listen(port);
|
|
20
22
|
server.on("error", (err) => {
|
|
21
23
|
if (err.code === "EADDRINUSE" && retries < maxRetries) {
|
|
22
24
|
retries++;
|
|
23
25
|
console.error(`[tezosx-mcp] Port ${port} in use, retrying (${retries}/${maxRetries})...`);
|
|
24
|
-
setTimeout(tryListen,
|
|
26
|
+
setTimeout(tryListen, retryDelay);
|
|
25
27
|
}
|
|
26
28
|
else if (err.code === "EADDRINUSE") {
|
|
27
|
-
console.error(`[tezosx-mcp] Port ${port} in use, frontend dashboard unavailable`);
|
|
29
|
+
console.error(`[tezosx-mcp] Port ${port} still in use after ${maxRetries} retries, frontend dashboard unavailable`);
|
|
28
30
|
}
|
|
29
31
|
else {
|
|
30
32
|
console.error(`[tezosx-mcp] Web server error:`, err.message);
|
|
31
33
|
}
|
|
32
34
|
});
|
|
33
35
|
server.on("listening", () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
process.on("SIGINT", shutdown);
|
|
37
|
-
process.on("exit", shutdown);
|
|
36
|
+
activeServer = server;
|
|
37
|
+
console.error(`[tezosx-mcp] Dashboard running on http://localhost:${port}`);
|
|
38
38
|
});
|
|
39
39
|
};
|
|
40
|
+
// Ensure the server is fully closed before the process exits
|
|
41
|
+
const shutdown = () => {
|
|
42
|
+
if (activeServer) {
|
|
43
|
+
activeServer.closeAllConnections();
|
|
44
|
+
activeServer.close();
|
|
45
|
+
activeServer = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
process.on("SIGTERM", () => { shutdown(); process.exit(0); });
|
|
49
|
+
process.on("SIGINT", () => { shutdown(); process.exit(0); });
|
|
40
50
|
tryListen();
|
|
41
51
|
};
|