@ecadlabs/tezosx-mcp 1.0.1 → 1.0.3
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 +2 -2
- package/dist/live-config.d.ts +6 -0
- package/dist/live-config.js +14 -0
- package/dist/tools/create_x402_payment.js +7 -8
- package/dist/tools/fetch_with_x402.d.ts +1 -1
- package/dist/tools/fetch_with_x402.js +27 -18
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/send_xtz.d.ts +1 -1
- package/dist/tools/send_xtz.js +2 -5
- package/dist/tools/x402/sign.d.ts +1 -0
- package/dist/tools/x402/sign.js +24 -7
- package/dist/webserver.js +25 -1
- package/frontend/dist/assets/index-BM2KDhgo.js +278 -0
- package/frontend/dist/assets/index-DxuaKI2K.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -3
- package/frontend/dist/assets/index-B-2-_lot.js +0 -287
- package/frontend/dist/assets/index-CTdz8_ps.css +0 -1
package/dist/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { InMemorySigner } from '@taquito/signer';
|
|
3
|
-
import {
|
|
3
|
+
import { b58Encode, PrefixV2 } from '@taquito/utils';
|
|
4
4
|
import { randomBytes } from 'crypto';
|
|
5
5
|
import { configureLiveConfig, resetLiveConfig, NETWORKS } from './live-config.js';
|
|
6
6
|
import { savePendingKey, loadPendingKey, activatePendingKey, saveContract, clearConfig, loadConfig } from './config-store.js';
|
|
@@ -34,7 +34,7 @@ function localhostGuard(req, res, next) {
|
|
|
34
34
|
}
|
|
35
35
|
async function generateKeypair() {
|
|
36
36
|
const seed = randomBytes(32);
|
|
37
|
-
const secretKey =
|
|
37
|
+
const secretKey = b58Encode(seed, PrefixV2.Ed25519Seed);
|
|
38
38
|
const signer = await InMemorySigner.fromSecretKey(secretKey);
|
|
39
39
|
const publicKey = await signer.publicKey();
|
|
40
40
|
const address = await signer.publicKeyHash();
|
package/dist/live-config.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export interface LiveConfig {
|
|
|
6
6
|
tzktApi: string;
|
|
7
7
|
configured: boolean;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Calculate the fee rebate amount to keep the spender address funded for gas.
|
|
11
|
+
* Returns 0 if the spender has sufficient balance, otherwise the amount needed
|
|
12
|
+
* to bring the balance up to the target level.
|
|
13
|
+
*/
|
|
14
|
+
export declare function calculateFeeRebate(spenderBalanceMutez: number): number;
|
|
9
15
|
export declare const NETWORKS: {
|
|
10
16
|
readonly mainnet: {
|
|
11
17
|
readonly rpcUrl: "https://mainnet.tezos.ecadinfra.com";
|
package/dist/live-config.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { TezosToolkit } from "@taquito/taquito";
|
|
2
2
|
import { InMemorySigner } from "@taquito/signer";
|
|
3
|
+
// Spender fee rebate thresholds (in mutez)
|
|
4
|
+
const SPENDER_TOP_UP_THRESHOLD = 250_000; // 0.25 XTZ — rebate when below this
|
|
5
|
+
const SPENDER_TOP_UP_TARGET = 500_000; // 0.5 XTZ — rebate up to this level
|
|
6
|
+
/**
|
|
7
|
+
* Calculate the fee rebate amount to keep the spender address funded for gas.
|
|
8
|
+
* Returns 0 if the spender has sufficient balance, otherwise the amount needed
|
|
9
|
+
* to bring the balance up to the target level.
|
|
10
|
+
*/
|
|
11
|
+
export function calculateFeeRebate(spenderBalanceMutez) {
|
|
12
|
+
if (spenderBalanceMutez < SPENDER_TOP_UP_THRESHOLD) {
|
|
13
|
+
return SPENDER_TOP_UP_TARGET - spenderBalanceMutez;
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
3
17
|
export const NETWORKS = {
|
|
4
18
|
mainnet: {
|
|
5
19
|
rpcUrl: 'https://mainnet.tezos.ecadinfra.com',
|
|
@@ -29,19 +29,18 @@ export const createCreateX402PaymentTool = (config) => ({
|
|
|
29
29
|
if (isNaN(amountMutez) || amountMutez <= 0) {
|
|
30
30
|
throw new Error(`Invalid amount: ${amount}. Must be a positive integer in mutez.`);
|
|
31
31
|
}
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`Required: ${amountMutez + 10000} mutez (including fees), ` +
|
|
39
|
-
`Available: ${sourceBalance.toNumber()} mutez`);
|
|
32
|
+
// Validate contract has sufficient funds for the payment
|
|
33
|
+
const contractBalance = await Tezos.tz.getBalance(config.spendingContract);
|
|
34
|
+
if (contractBalance.toNumber() < amountMutez) {
|
|
35
|
+
throw new Error(`Insufficient contract balance. ` +
|
|
36
|
+
`Required: ${amountMutez} mutez, ` +
|
|
37
|
+
`Available: ${contractBalance.toNumber()} mutez`);
|
|
40
38
|
}
|
|
41
39
|
const signed = await signX402Payment(Tezos, {
|
|
42
40
|
network,
|
|
43
41
|
amount: amountMutez,
|
|
44
42
|
recipient,
|
|
43
|
+
spendingContract: config.spendingContract,
|
|
45
44
|
});
|
|
46
45
|
return {
|
|
47
46
|
content: [{
|
|
@@ -10,7 +10,7 @@ export declare const createFetchWithX402Tool: (config: LiveConfig) => {
|
|
|
10
10
|
maxAmountMutez: z.ZodString;
|
|
11
11
|
method: z.ZodOptional<z.ZodString>;
|
|
12
12
|
body: z.ZodOptional<z.ZodString>;
|
|
13
|
-
|
|
13
|
+
queryParams: z.ZodOptional<z.ZodString>;
|
|
14
14
|
}, z.z.core.$strip>;
|
|
15
15
|
annotations: {
|
|
16
16
|
readOnlyHint: boolean;
|
|
@@ -7,13 +7,13 @@ export const createFetchWithX402Tool = (config) => ({
|
|
|
7
7
|
name: "tezos_fetch_with_x402",
|
|
8
8
|
config: {
|
|
9
9
|
title: "Fetch with x402 Payment",
|
|
10
|
-
description: "Fetches a URL and automatically handles x402 payment requirements. If the server returns 402, it parses the requirements, creates a signed payment, and retries the request with the X-PAYMENT header.
|
|
10
|
+
description: "Fetches a URL and automatically handles x402 payment requirements. If the server returns 402, it parses the requirements, creates a signed payment, and retries the request with the X-PAYMENT header. Use 'queryParams' to pass dynamic parameters to the endpoint (e.g., for NFT minting, pass {\"recipient\": \"tz1...\"} so the NFT goes to the user's wallet instead of the spender account).",
|
|
11
11
|
inputSchema: z.object({
|
|
12
12
|
url: z.string().describe("The URL to fetch"),
|
|
13
13
|
maxAmountMutez: z.string().describe("Maximum amount in mutez willing to pay (e.g., '500000' for 0.5 XTZ)"),
|
|
14
14
|
method: z.string().optional().describe("HTTP method (default: GET)"),
|
|
15
|
-
body: z.string().optional().describe("Request body for POST/PUT requests"),
|
|
16
|
-
|
|
15
|
+
body: z.string().optional().describe("Request body for POST/PUT requests (JSON string)"),
|
|
16
|
+
queryParams: z.string().optional().describe("JSON object of query parameters to append to the URL (e.g., '{\"recipient\": \"tz1...\", \"amount\": \"100\"}')"),
|
|
17
17
|
}),
|
|
18
18
|
annotations: {
|
|
19
19
|
readOnlyHint: false,
|
|
@@ -24,17 +24,27 @@ export const createFetchWithX402Tool = (config) => ({
|
|
|
24
24
|
},
|
|
25
25
|
handler: async (params) => {
|
|
26
26
|
const { Tezos } = config;
|
|
27
|
-
const { url, maxAmountMutez, method = "GET", body,
|
|
27
|
+
const { url, maxAmountMutez, method = "GET", body, queryParams } = params;
|
|
28
28
|
const maxAmount = parseInt(maxAmountMutez, 10);
|
|
29
29
|
if (isNaN(maxAmount) || maxAmount <= 0) {
|
|
30
30
|
throw new Error(`Invalid maxAmountMutez: ${maxAmountMutez}. Must be a positive integer.`);
|
|
31
31
|
}
|
|
32
|
-
// Build URL with
|
|
32
|
+
// Build URL with query parameters if specified
|
|
33
33
|
let requestUrl = url;
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
if (queryParams) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(queryParams);
|
|
37
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
38
|
+
const urlObj = new URL(url);
|
|
39
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
40
|
+
urlObj.searchParams.set(key, String(value));
|
|
41
|
+
}
|
|
42
|
+
requestUrl = urlObj.toString();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error(`Invalid queryParams: must be a valid JSON object (e.g., '{"key": "value"}')`);
|
|
47
|
+
}
|
|
38
48
|
}
|
|
39
49
|
// Initial request config
|
|
40
50
|
const axiosConfig = {
|
|
@@ -87,22 +97,21 @@ export const createFetchWithX402Tool = (config) => ({
|
|
|
87
97
|
const maxFormatted = maxAmount / Math.pow(10, decimals);
|
|
88
98
|
throw new Error(`Payment amount ${requiredFormatted} XTZ exceeds maximum allowed ${maxFormatted} XTZ`);
|
|
89
99
|
}
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`Required: ${requiredAmount + 10000} mutez (including fees), ` +
|
|
97
|
-
`Available: ${sourceBalance.toNumber()} mutez`);
|
|
100
|
+
// Validate contract has sufficient funds for the payment
|
|
101
|
+
const contractBalance = await Tezos.tz.getBalance(config.spendingContract);
|
|
102
|
+
if (contractBalance.toNumber() < requiredAmount) {
|
|
103
|
+
throw new Error(`Insufficient contract balance. ` +
|
|
104
|
+
`Required: ${requiredAmount} mutez, ` +
|
|
105
|
+
`Available: ${contractBalance.toNumber()} mutez`);
|
|
98
106
|
}
|
|
99
107
|
// Ensure account is revealed before signing
|
|
100
108
|
await ensureRevealed(Tezos);
|
|
101
|
-
// Sign the payment
|
|
109
|
+
// Sign the payment as a contract call to the spending wallet
|
|
102
110
|
const signed = await signX402Payment(Tezos, {
|
|
103
111
|
network: tezosRequirement.network,
|
|
104
112
|
amount: requiredAmount,
|
|
105
113
|
recipient: tezosRequirement.recipient,
|
|
114
|
+
spendingContract: config.spendingContract,
|
|
106
115
|
});
|
|
107
116
|
// Retry with payment header
|
|
108
117
|
const paidConfig = {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export declare const createTools: (liveConfig: LiveConfig, http: boolean) => ({
|
|
|
52
52
|
maxAmountMutez: import("zod").ZodString;
|
|
53
53
|
method: import("zod").ZodOptional<import("zod").ZodString>;
|
|
54
54
|
body: import("zod").ZodOptional<import("zod").ZodString>;
|
|
55
|
-
|
|
55
|
+
queryParams: import("zod").ZodOptional<import("zod").ZodString>;
|
|
56
56
|
}, import("zod/v4/core").$strip>;
|
|
57
57
|
annotations: {
|
|
58
58
|
readOnlyHint: boolean;
|
package/dist/tools/send_xtz.d.ts
CHANGED
package/dist/tools/send_xtz.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import { ensureRevealed } from "./reveal_account.js";
|
|
3
|
+
import { calculateFeeRebate } from "../live-config.js";
|
|
3
4
|
// Constants
|
|
4
5
|
const MUTEZ_PER_TEZ = 1_000_000;
|
|
5
6
|
const CONFIRMATIONS_TO_WAIT = 3;
|
|
6
7
|
const TZKT_BASE_URL = "https://shadownet.tzkt.io";
|
|
7
|
-
const SPENDER_TOP_UP_THRESHOLD = 250_000; // 0.25 XTZ
|
|
8
|
-
const SPENDER_TOP_UP_TARGET = 500_000; // 0.5 XTZ
|
|
9
8
|
// Types
|
|
10
9
|
const inputSchema = z.object({
|
|
11
10
|
toAddress: z.string().describe("The address to send Tez to."),
|
|
@@ -67,9 +66,7 @@ export const createSendXtzTool = (config) => ({
|
|
|
67
66
|
await ensureRevealed(Tezos);
|
|
68
67
|
// Top up spender from contract if balance is low
|
|
69
68
|
const spenderMutez = spenderBalance.toNumber();
|
|
70
|
-
const feeRebate = spenderMutez
|
|
71
|
-
? SPENDER_TOP_UP_TARGET - spenderMutez
|
|
72
|
-
: 0;
|
|
69
|
+
const feeRebate = calculateFeeRebate(spenderMutez);
|
|
73
70
|
const contract = await Tezos.contract.at(spendingContract);
|
|
74
71
|
const contractCall = contract.methodsObject.spend({
|
|
75
72
|
recipient: params.toAddress,
|
package/dist/tools/x402/sign.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { OpKind } from "@taquito/taquito";
|
|
2
2
|
import { LocalForger } from "@taquito/local-forging";
|
|
3
|
+
import { calculateFeeRebate } from "../../live-config.js";
|
|
3
4
|
export async function signX402Payment(Tezos, params) {
|
|
4
|
-
const { network, amount, recipient } = params;
|
|
5
|
+
const { network, amount, recipient, spendingContract } = params;
|
|
5
6
|
// Get source address and public key from the signer
|
|
6
7
|
let source;
|
|
7
8
|
let publicKey;
|
|
@@ -24,6 +25,21 @@ export async function signX402Payment(Tezos, params) {
|
|
|
24
25
|
if (!publicKey.startsWith('edpk') && !publicKey.startsWith('sppk') && !publicKey.startsWith('p2pk')) {
|
|
25
26
|
throw new Error(`Invalid public key format: ${publicKey}`);
|
|
26
27
|
}
|
|
28
|
+
// Calculate fee rebate to keep spender funded for gas
|
|
29
|
+
const spenderBalance = await Tezos.tz.getBalance(source);
|
|
30
|
+
const spenderMutez = spenderBalance.toNumber();
|
|
31
|
+
const feeRebate = calculateFeeRebate(spenderMutez);
|
|
32
|
+
// Build contract call via Taquito to get properly encoded parameters
|
|
33
|
+
const contract = await Tezos.contract.at(spendingContract);
|
|
34
|
+
const contractCall = contract.methodsObject.spend({
|
|
35
|
+
recipient,
|
|
36
|
+
amount,
|
|
37
|
+
fee_rebate: feeRebate,
|
|
38
|
+
});
|
|
39
|
+
// Estimate gas, storage, and fees
|
|
40
|
+
const estimate = await Tezos.estimate.contractCall(contractCall);
|
|
41
|
+
// Get the encoded parameters from the contract call
|
|
42
|
+
const transferParams = contractCall.toTransferParams();
|
|
27
43
|
// Get the current block hash for the branch
|
|
28
44
|
const block = await Tezos.rpc.getBlockHeader();
|
|
29
45
|
const branch = block.hash;
|
|
@@ -33,19 +49,20 @@ export async function signX402Payment(Tezos, params) {
|
|
|
33
49
|
throw new Error(`Failed to get contract info for ${source}. Account may not exist on chain.`);
|
|
34
50
|
}
|
|
35
51
|
const nextCounter = (parseInt(contractInfo.counter || "0") + 1).toString();
|
|
36
|
-
// Build the
|
|
52
|
+
// Build the contract call operation (funds come from the contract, not the spender)
|
|
37
53
|
const operation = {
|
|
38
54
|
branch,
|
|
39
55
|
contents: [
|
|
40
56
|
{
|
|
41
57
|
kind: OpKind.TRANSACTION,
|
|
42
58
|
source,
|
|
43
|
-
fee:
|
|
59
|
+
fee: estimate.suggestedFeeMutez.toString(),
|
|
44
60
|
counter: nextCounter,
|
|
45
|
-
gas_limit:
|
|
46
|
-
storage_limit:
|
|
47
|
-
amount:
|
|
48
|
-
destination:
|
|
61
|
+
gas_limit: estimate.gasLimit.toString(),
|
|
62
|
+
storage_limit: estimate.storageLimit.toString(),
|
|
63
|
+
amount: "0",
|
|
64
|
+
destination: spendingContract,
|
|
65
|
+
parameters: transferParams.parameter,
|
|
49
66
|
}
|
|
50
67
|
]
|
|
51
68
|
};
|
package/dist/webserver.js
CHANGED
|
@@ -13,5 +13,29 @@ export const startWebServer = (port, apiRouter) => {
|
|
|
13
13
|
// Static files + SPA fallback
|
|
14
14
|
const distPath = join(__dirname, "../frontend/dist");
|
|
15
15
|
app.use(sirv(distPath, { single: true }));
|
|
16
|
-
|
|
16
|
+
let retries = 0;
|
|
17
|
+
const maxRetries = 3;
|
|
18
|
+
const tryListen = () => {
|
|
19
|
+
const server = app.listen(port);
|
|
20
|
+
server.on("error", (err) => {
|
|
21
|
+
if (err.code === "EADDRINUSE" && retries < maxRetries) {
|
|
22
|
+
retries++;
|
|
23
|
+
console.error(`[tezosx-mcp] Port ${port} in use, retrying (${retries}/${maxRetries})...`);
|
|
24
|
+
setTimeout(tryListen, 500);
|
|
25
|
+
}
|
|
26
|
+
else if (err.code === "EADDRINUSE") {
|
|
27
|
+
console.error(`[tezosx-mcp] Port ${port} in use, frontend dashboard unavailable`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.error(`[tezosx-mcp] Web server error:`, err.message);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
server.on("listening", () => {
|
|
34
|
+
const shutdown = () => server.close();
|
|
35
|
+
process.on("SIGTERM", shutdown);
|
|
36
|
+
process.on("SIGINT", shutdown);
|
|
37
|
+
process.on("exit", shutdown);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
tryListen();
|
|
17
41
|
};
|