@ecadlabs/tezosx-mcp 1.0.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.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +335 -0
  3. package/dist/adapters/index.d.ts +37 -0
  4. package/dist/adapters/index.js +57 -0
  5. package/dist/adapters/node.d.ts +18 -0
  6. package/dist/adapters/node.js +35 -0
  7. package/dist/adapters/types.d.ts +52 -0
  8. package/dist/adapters/types.js +25 -0
  9. package/dist/adapters/worker.d.ts +35 -0
  10. package/dist/adapters/worker.js +50 -0
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.js +144 -0
  13. package/dist/server.d.ts +36 -0
  14. package/dist/server.js +80 -0
  15. package/dist/tools/create_x402_payment.d.ts +27 -0
  16. package/dist/tools/create_x402_payment.js +55 -0
  17. package/dist/tools/fetch_with_x402.d.ts +28 -0
  18. package/dist/tools/fetch_with_x402.js +143 -0
  19. package/dist/tools/get_address.d.ts +20 -0
  20. package/dist/tools/get_address.js +24 -0
  21. package/dist/tools/get_addresses.d.ts +22 -0
  22. package/dist/tools/get_addresses.js +32 -0
  23. package/dist/tools/get_balance.d.ts +22 -0
  24. package/dist/tools/get_balance.js +27 -0
  25. package/dist/tools/get_dashboard.d.ts +21 -0
  26. package/dist/tools/get_dashboard.js +29 -0
  27. package/dist/tools/get_limits.d.ts +22 -0
  28. package/dist/tools/get_limits.js +61 -0
  29. package/dist/tools/get_operation_history.d.ts +21 -0
  30. package/dist/tools/get_operation_history.js +58 -0
  31. package/dist/tools/index.d.ts +113 -0
  32. package/dist/tools/index.js +59 -0
  33. package/dist/tools/parse_x402_requirements.d.ts +23 -0
  34. package/dist/tools/parse_x402_requirements.js +66 -0
  35. package/dist/tools/reveal_account.d.ts +34 -0
  36. package/dist/tools/reveal_account.js +51 -0
  37. package/dist/tools/send_xtz.d.ts +32 -0
  38. package/dist/tools/send_xtz.js +86 -0
  39. package/dist/tools/x402/sign.d.ts +12 -0
  40. package/dist/tools/x402/sign.js +76 -0
  41. package/dist/tools/x402/types.d.ts +40 -0
  42. package/dist/tools/x402/types.js +16 -0
  43. package/dist/webserver.d.ts +1 -0
  44. package/dist/webserver.js +10 -0
  45. package/dist/worker.bundle.js +134265 -0
  46. package/dist/worker.d.ts +13 -0
  47. package/dist/worker.js +132 -0
  48. package/frontend/dist/assets/index-RtTL1nIl.js +257 -0
  49. package/frontend/dist/assets/index-mSsI3AqQ.css +1 -0
  50. package/frontend/dist/index.html +16 -0
  51. package/package.json +70 -0
@@ -0,0 +1,59 @@
1
+ import { createCreateX402PaymentTool } from "./create_x402_payment.js";
2
+ import { createFetchWithX402Tool } from "./fetch_with_x402.js";
3
+ import { createGetAddressesTool } from "./get_addresses.js";
4
+ import { createGetBalanceTool } from "./get_balance.js";
5
+ import { createGetLimitsTool } from "./get_limits.js";
6
+ import { createGetOperationHistoryTool } from "./get_operation_history.js";
7
+ import { createParseX402RequirementsTool } from "./parse_x402_requirements.js";
8
+ import { createRevealAccountTool } from "./reveal_account.js";
9
+ import { createSendXtzTool } from "./send_xtz.js";
10
+ import { createGetDashboardTool } from "./get_dashboard.js";
11
+ const getNotConfiguredMessage = () => `Wallet not configured. Please set the following environment variables and restart the MCP server:
12
+
13
+ 1. SPENDING_PRIVATE_KEY - Your spending key (starts with edsk, spsk, or p2sk)
14
+ 2. SPENDING_CONTRACT - Your spending contract address (starts with KT1)
15
+
16
+ To get started:
17
+ 1. Open the dashboard at http://localhost:${process.env.WEB_PORT || '13205'}
18
+ 2. Connect your wallet and deploy a spending contract
19
+ 3. Add the environment variables to your MCP client configuration
20
+ 4. Restart the MCP server`;
21
+ const notConfiguredResponse = () => ({
22
+ content: [{
23
+ type: "text",
24
+ text: getNotConfiguredMessage()
25
+ }]
26
+ });
27
+ // Wraps a tool's handler to check config before execution
28
+ const withConfigCheck = (tool, walletConfig) => ({
29
+ ...tool,
30
+ handler: async (...args) => {
31
+ if (!walletConfig) {
32
+ return notConfiguredResponse();
33
+ }
34
+ return tool.handler(...args);
35
+ }
36
+ });
37
+ export const createTools = (walletConfig, tzktApi, http) => {
38
+ // Create a mock Tezos toolkit for tool creation when config is missing
39
+ // The actual handlers will be blocked by withConfigCheck
40
+ const Tezos = walletConfig?.Tezos ?? {};
41
+ const spendingContract = walletConfig?.spendingContract ?? '';
42
+ const spendingAddress = walletConfig?.spendingAddress ?? '';
43
+ const tools = [
44
+ createCreateX402PaymentTool(Tezos),
45
+ createFetchWithX402Tool(Tezos),
46
+ createGetAddressesTool(Tezos, spendingContract),
47
+ createGetBalanceTool(Tezos, spendingContract, spendingAddress),
48
+ createGetLimitsTool(Tezos, spendingContract),
49
+ createGetOperationHistoryTool(spendingContract, tzktApi),
50
+ createParseX402RequirementsTool(),
51
+ createRevealAccountTool(Tezos),
52
+ createSendXtzTool(Tezos, spendingContract, spendingAddress),
53
+ ];
54
+ if (!http) {
55
+ tools.push(createGetDashboardTool(spendingContract));
56
+ }
57
+ // Wrap all tools with config check
58
+ return tools.map(tool => withConfigCheck(tool, walletConfig));
59
+ };
@@ -0,0 +1,23 @@
1
+ import z from "zod";
2
+ export declare const createParseX402RequirementsTool: () => {
3
+ name: string;
4
+ config: {
5
+ title: string;
6
+ description: string;
7
+ inputSchema: z.ZodObject<{
8
+ responseBody: z.ZodString;
9
+ }, z.z.core.$strip>;
10
+ annotations: {
11
+ readOnlyHint: boolean;
12
+ destructiveHint: boolean;
13
+ idempotentHint: boolean;
14
+ openWorldHint: boolean;
15
+ };
16
+ };
17
+ handler: (params: any) => Promise<{
18
+ content: {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ }>;
23
+ };
@@ -0,0 +1,66 @@
1
+ import z from "zod";
2
+ import { X402ResponseSchema } from "./x402/types.js";
3
+ export const createParseX402RequirementsTool = () => ({
4
+ name: "tezos_parse_x402_requirements",
5
+ config: {
6
+ title: "Parse x402 Payment Requirements",
7
+ description: "Parses a 402 response body containing x402 payment requirements for Tezos payments. Returns structured payment information including scheme, network, asset, amount, and recipient.",
8
+ inputSchema: z.object({
9
+ responseBody: z.string().describe("The JSON response body from a 402 Payment Required response")
10
+ }),
11
+ annotations: {
12
+ readOnlyHint: true,
13
+ destructiveHint: false,
14
+ idempotentHint: true,
15
+ openWorldHint: false,
16
+ }
17
+ },
18
+ handler: async (params) => {
19
+ const { responseBody } = params;
20
+ try {
21
+ const parsed = JSON.parse(responseBody);
22
+ const validated = X402ResponseSchema.parse(parsed);
23
+ const tezosRequirements = validated.paymentRequirements.filter(req => req.scheme === 'exact-tezos');
24
+ if (tezosRequirements.length === 0) {
25
+ return {
26
+ content: [{
27
+ type: "text",
28
+ text: "No Tezos payment requirements found in the response"
29
+ }]
30
+ };
31
+ }
32
+ const formattedRequirements = tezosRequirements.map(req => {
33
+ const decimals = req.extra?.decimals ?? 6;
34
+ const amountInUnits = Number(req.amount) / Math.pow(10, decimals);
35
+ const assetName = req.extra?.name ?? req.asset;
36
+ return {
37
+ network: req.network,
38
+ asset: req.asset,
39
+ assetName,
40
+ amount: req.amount,
41
+ amountFormatted: `${amountInUnits} ${assetName}`,
42
+ recipient: req.recipient,
43
+ decimals
44
+ };
45
+ });
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: JSON.stringify({
50
+ x402Version: validated.x402Version,
51
+ tezosPaymentRequirements: formattedRequirements
52
+ }, null, 2)
53
+ }]
54
+ };
55
+ }
56
+ catch (error) {
57
+ if (error instanceof z.ZodError) {
58
+ throw new Error(`Invalid x402 response format: ${error.message}`);
59
+ }
60
+ if (error instanceof SyntaxError) {
61
+ throw new Error(`Invalid JSON: ${error.message}`);
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ });
@@ -0,0 +1,34 @@
1
+ import { TezosToolkit } from "@taquito/taquito";
2
+ import z from "zod";
3
+ /**
4
+ * Check if an account is revealed on the Tezos network.
5
+ */
6
+ export declare function isAccountRevealed(Tezos: TezosToolkit, address: string): Promise<boolean>;
7
+ /**
8
+ * Reveal an account if not already revealed.
9
+ * Returns true if reveal was needed and performed, false if already revealed.
10
+ */
11
+ export declare function ensureRevealed(Tezos: TezosToolkit): Promise<{
12
+ wasRevealed: boolean;
13
+ opHash?: string;
14
+ }>;
15
+ export declare const createRevealAccountTool: (Tezos: TezosToolkit) => {
16
+ name: string;
17
+ config: {
18
+ title: string;
19
+ description: string;
20
+ inputSchema: z.ZodObject<{}, z.z.core.$strip>;
21
+ annotations: {
22
+ readOnlyHint: boolean;
23
+ destructiveHint: boolean;
24
+ idempotentHint: boolean;
25
+ openWorldHint: boolean;
26
+ };
27
+ };
28
+ handler: () => Promise<{
29
+ content: {
30
+ type: "text";
31
+ text: string;
32
+ }[];
33
+ }>;
34
+ };
@@ -0,0 +1,51 @@
1
+ import z from "zod";
2
+ /**
3
+ * Check if an account is revealed on the Tezos network.
4
+ */
5
+ export async function isAccountRevealed(Tezos, address) {
6
+ const managerKey = await Tezos.rpc.getManagerKey(address);
7
+ return managerKey !== null;
8
+ }
9
+ /**
10
+ * Reveal an account if not already revealed.
11
+ * Returns true if reveal was needed and performed, false if already revealed.
12
+ */
13
+ export async function ensureRevealed(Tezos) {
14
+ const address = await Tezos.signer.publicKeyHash();
15
+ const revealed = await isAccountRevealed(Tezos, address);
16
+ if (revealed) {
17
+ return { wasRevealed: false };
18
+ }
19
+ const op = await Tezos.contract.reveal({});
20
+ await op.confirmation(1);
21
+ return { wasRevealed: true, opHash: op.hash };
22
+ }
23
+ export const createRevealAccountTool = (Tezos) => ({
24
+ name: "tezos_reveal_account",
25
+ config: {
26
+ title: "Reveal Account",
27
+ description: "Reveals the spender account's public key on the Tezos network. This is required before the account can perform any operations. The tool checks if the account is already revealed and only performs the reveal operation if needed.",
28
+ inputSchema: z.object({}),
29
+ annotations: {
30
+ readOnlyHint: false,
31
+ destructiveHint: false,
32
+ idempotentHint: true,
33
+ openWorldHint: false,
34
+ }
35
+ },
36
+ handler: async () => {
37
+ const address = await Tezos.signer.publicKeyHash();
38
+ const result = await ensureRevealed(Tezos);
39
+ return {
40
+ content: [{
41
+ type: "text",
42
+ text: JSON.stringify({
43
+ success: true,
44
+ message: result.wasRevealed ? `Account ${address} has been revealed` : `Account ${address} was already revealed`,
45
+ operationHash: result.opHash,
46
+ alreadyRevealed: !result.wasRevealed,
47
+ }, null, 2)
48
+ }]
49
+ };
50
+ }
51
+ });
@@ -0,0 +1,32 @@
1
+ import { TezosToolkit } from "@taquito/taquito";
2
+ import z from "zod";
3
+ /**
4
+ * MCP tool for sending XTZ via a spending contract.
5
+ *
6
+ * @param Tezos - Configured TezosToolkit instance (with signer set to spender key)
7
+ * @param spendingContract - Address of the spending-limited wallet contract
8
+ * @param spendingAddress - Address of the spender account (for fee payments)
9
+ */
10
+ export declare const createSendXtzTool: (Tezos: TezosToolkit, spendingContract: string, spendingAddress: string) => {
11
+ name: string;
12
+ config: {
13
+ title: string;
14
+ description: string;
15
+ inputSchema: z.ZodObject<{
16
+ toAddress: z.ZodString;
17
+ amount: z.ZodNumber;
18
+ }, z.z.core.$strip>;
19
+ annotations: {
20
+ readOnlyHint: boolean;
21
+ destructiveHint: boolean;
22
+ idempotentHint: boolean;
23
+ openWorldHint: boolean;
24
+ };
25
+ };
26
+ handler: (params: any) => Promise<{
27
+ content: {
28
+ type: "text";
29
+ text: string;
30
+ }[];
31
+ }>;
32
+ };
@@ -0,0 +1,86 @@
1
+ import z from "zod";
2
+ // Constants
3
+ const MUTEZ_PER_TEZ = 1_000_000;
4
+ const CONFIRMATIONS_TO_WAIT = 3;
5
+ const TZKT_BASE_URL = "https://shadownet.tzkt.io";
6
+ // Types
7
+ const inputSchema = z.object({
8
+ toAddress: z.string().describe("The address to send Tez to."),
9
+ amount: z.number().describe("The amount of Tez to send to the address."),
10
+ });
11
+ // Helper Functions
12
+ /** Convert XTZ to mutez */
13
+ const xtzToMutez = (xtz) => xtz * MUTEZ_PER_TEZ;
14
+ /** Format mutez for display */
15
+ const formatMutez = (mutez) => `${mutez} mutez`;
16
+ /**
17
+ * MCP tool for sending XTZ via a spending contract.
18
+ *
19
+ * @param Tezos - Configured TezosToolkit instance (with signer set to spender key)
20
+ * @param spendingContract - Address of the spending-limited wallet contract
21
+ * @param spendingAddress - Address of the spender account (for fee payments)
22
+ */
23
+ export const createSendXtzTool = (Tezos, spendingContract, spendingAddress) => ({
24
+ name: "tezos_send_xtz",
25
+ config: {
26
+ title: "Send Tez",
27
+ description: "Sends a set amount of Tez to another address via the spending contract.",
28
+ inputSchema,
29
+ annotations: {
30
+ readOnlyHint: false,
31
+ destructiveHint: true,
32
+ idempotentHint: false,
33
+ openWorldHint: true,
34
+ },
35
+ },
36
+ handler: async (params) => {
37
+ params = params;
38
+ // Validate spender has funds for fees
39
+ const spenderBalance = await Tezos.tz.getBalance(spendingAddress);
40
+ if (spenderBalance.isZero()) {
41
+ throw new Error("Spending account balance is 0. " +
42
+ "Please fund the spending address to cover transaction fees.");
43
+ }
44
+ // Validate contract has funds for transfer
45
+ const contractBalance = await Tezos.tz.getBalance(spendingContract);
46
+ const amountMutez = xtzToMutez(params.amount);
47
+ if (contractBalance.toNumber() < amountMutez) {
48
+ throw new Error(`Insufficient contract balance. ` +
49
+ `Requested: ${formatMutez(amountMutez)}, ` +
50
+ `Available: ${formatMutez(contractBalance.toNumber())}`);
51
+ }
52
+ // Prepare contract call
53
+ const contract = await Tezos.contract.at(spendingContract);
54
+ const contractCall = contract.methodsObject.spend({
55
+ recipient: params.toAddress,
56
+ amount: amountMutez,
57
+ });
58
+ // Estimate fees
59
+ let estimate;
60
+ try {
61
+ estimate = await Tezos.estimate.contractCall(contractCall);
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ if (message.includes("balance_too_low")) {
66
+ throw new Error(`Spender balance (${formatMutez(spenderBalance.toNumber())}) ` +
67
+ `is too low to cover fees. Please fund the spending address.`);
68
+ }
69
+ throw err;
70
+ }
71
+ // Run another fee check because the first check is only estimating the fees, but still needs tez in the account to estimate.
72
+ // This check is checking against the actual estimated fee value.
73
+ if (spenderBalance.toNumber() < estimate.totalCost) {
74
+ throw new Error(`Spender balance too low for fees. ` +
75
+ `Required: ${formatMutez(estimate.totalCost)}, ` +
76
+ `Available: ${formatMutez(spenderBalance.toNumber())}`);
77
+ }
78
+ // Execute transaction
79
+ const operation = await contractCall.send();
80
+ await operation.confirmation(CONFIRMATIONS_TO_WAIT);
81
+ const tzktUrl = `${TZKT_BASE_URL}/${operation.hash}`;
82
+ return {
83
+ content: [{ type: "text", text: tzktUrl }],
84
+ };
85
+ },
86
+ });
@@ -0,0 +1,12 @@
1
+ import { TezosToolkit } from "@taquito/taquito";
2
+ import { X402PaymentPayload } from "./types.js";
3
+ export interface SignPaymentParams {
4
+ network: string;
5
+ amount: number;
6
+ recipient: string;
7
+ }
8
+ export interface SignedPayment {
9
+ payload: X402PaymentPayload;
10
+ base64: string;
11
+ }
12
+ export declare function signX402Payment(Tezos: TezosToolkit, params: SignPaymentParams): Promise<SignedPayment>;
@@ -0,0 +1,76 @@
1
+ import { OpKind } from "@taquito/taquito";
2
+ import { LocalForger } from "@taquito/local-forging";
3
+ export async function signX402Payment(Tezos, params) {
4
+ const { network, amount, recipient } = params;
5
+ // Get source address and public key from the signer
6
+ let source;
7
+ let publicKey;
8
+ try {
9
+ source = await Tezos.signer.publicKeyHash();
10
+ }
11
+ catch (error) {
12
+ throw new Error(`Failed to get source address from signer. Is SPENDING_PRIVATE_KEY set correctly? Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
13
+ }
14
+ try {
15
+ publicKey = await Tezos.signer.publicKey();
16
+ }
17
+ catch (error) {
18
+ throw new Error(`Failed to get public key from signer. Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
19
+ }
20
+ // Validate we got proper values
21
+ if (!source.startsWith('tz1') && !source.startsWith('tz2') && !source.startsWith('tz3')) {
22
+ throw new Error(`Invalid source address format: ${source}`);
23
+ }
24
+ if (!publicKey.startsWith('edpk') && !publicKey.startsWith('sppk') && !publicKey.startsWith('p2pk')) {
25
+ throw new Error(`Invalid public key format: ${publicKey}`);
26
+ }
27
+ // Get the current block hash for the branch
28
+ const block = await Tezos.rpc.getBlockHeader();
29
+ const branch = block.hash;
30
+ // Get the counter for the source account
31
+ const contractInfo = await Tezos.rpc.getContract(source);
32
+ if (!contractInfo) {
33
+ throw new Error(`Failed to get contract info for ${source}. Account may not exist on chain.`);
34
+ }
35
+ const nextCounter = (parseInt(contractInfo.counter || "0") + 1).toString();
36
+ // Build the transfer operation
37
+ const operation = {
38
+ branch,
39
+ contents: [
40
+ {
41
+ kind: OpKind.TRANSACTION,
42
+ source,
43
+ fee: "1500",
44
+ counter: nextCounter,
45
+ gas_limit: "1527",
46
+ storage_limit: "257",
47
+ amount: amount.toString(),
48
+ destination: recipient,
49
+ }
50
+ ]
51
+ };
52
+ // Forge the operation using LocalForger
53
+ const forger = new LocalForger();
54
+ const forgedBytes = await forger.forge(operation);
55
+ // Sign the operation (with generic operation watermark 0x03)
56
+ const signature = await Tezos.signer.sign(forgedBytes, new Uint8Array([3]));
57
+ // Create x402 payload in the facilitator-compatible format
58
+ const payload = {
59
+ x402Version: 1,
60
+ scheme: "exact-tezos",
61
+ network,
62
+ asset: "XTZ",
63
+ payload: {
64
+ operationBytes: forgedBytes,
65
+ signature: signature.prefixSig,
66
+ publicKey,
67
+ source,
68
+ }
69
+ };
70
+ // Base64 encode the payload
71
+ const base64 = Buffer.from(JSON.stringify(payload)).toString("base64");
72
+ return {
73
+ payload,
74
+ base64,
75
+ };
76
+ }
@@ -0,0 +1,40 @@
1
+ import z from "zod";
2
+ export declare const PaymentRequirementSchema: z.ZodObject<{
3
+ scheme: z.ZodString;
4
+ network: z.ZodString;
5
+ asset: z.ZodString;
6
+ amount: z.ZodString;
7
+ recipient: z.ZodString;
8
+ extra: z.ZodOptional<z.ZodObject<{
9
+ name: z.ZodString;
10
+ decimals: z.ZodNumber;
11
+ }, z.z.core.$strip>>;
12
+ }, z.z.core.$strip>;
13
+ export declare const X402ResponseSchema: z.ZodObject<{
14
+ x402Version: z.ZodNumber;
15
+ paymentRequirements: z.ZodArray<z.ZodObject<{
16
+ scheme: z.ZodString;
17
+ network: z.ZodString;
18
+ asset: z.ZodString;
19
+ amount: z.ZodString;
20
+ recipient: z.ZodString;
21
+ extra: z.ZodOptional<z.ZodObject<{
22
+ name: z.ZodString;
23
+ decimals: z.ZodNumber;
24
+ }, z.z.core.$strip>>;
25
+ }, z.z.core.$strip>>;
26
+ }, z.z.core.$strip>;
27
+ export type PaymentRequirement = z.infer<typeof PaymentRequirementSchema>;
28
+ export type X402Response = z.infer<typeof X402ResponseSchema>;
29
+ export interface X402PaymentPayload {
30
+ x402Version: number;
31
+ scheme: "exact-tezos";
32
+ network: string;
33
+ asset: "XTZ";
34
+ payload: {
35
+ operationBytes: string;
36
+ signature: string;
37
+ publicKey: string;
38
+ source: string;
39
+ };
40
+ }
@@ -0,0 +1,16 @@
1
+ import z from "zod";
2
+ export const PaymentRequirementSchema = z.object({
3
+ scheme: z.string(),
4
+ network: z.string(),
5
+ asset: z.string(),
6
+ amount: z.string(),
7
+ recipient: z.string(),
8
+ extra: z.object({
9
+ name: z.string(),
10
+ decimals: z.number()
11
+ }).optional()
12
+ });
13
+ export const X402ResponseSchema = z.object({
14
+ x402Version: z.number(),
15
+ paymentRequirements: z.array(PaymentRequirementSchema)
16
+ });
@@ -0,0 +1 @@
1
+ export declare const startWebServer: (port: number) => void;
@@ -0,0 +1,10 @@
1
+ import { createServer } from "http";
2
+ import { fileURLToPath } from "url";
3
+ import { join } from "path";
4
+ import sirv from "sirv";
5
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
6
+ export const startWebServer = (port) => {
7
+ const distPath = join(__dirname, "../frontend/dist");
8
+ const serve = sirv(distPath, { single: true });
9
+ createServer(serve).listen(port);
10
+ };