@tonnode/mcp
Advanced tools
+145
| // generate_wallet — mint a fresh TON wallet (mnemonic + keypair + address) for | ||
| // an agent that needs one to operate. | ||
| // | ||
| // SECURITY POSTURE. This tool creates SECRET key material and returns it to the | ||
| // caller. In hosted (HTTP) mode that material is generated in this process and | ||
| // travels back over TLS — treat every generated wallet as HOT. The server | ||
| // itself never persists or logs the mnemonic/secret (only the public address is | ||
| // logged). Operators who do not want key material flowing through a shared | ||
| // endpoint can set TONNODE_DISABLE_WALLET_GEN=1 to unregister the tool. | ||
| import { z } from "zod"; | ||
| import { Cell, beginCell, contractAddress } from "@ton/core"; | ||
| import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto"; | ||
| import { WalletContractV3R2, WalletContractV4, WalletContractV5R1 } from "@ton/ton"; | ||
| import { ok, fail } from "./server.js"; | ||
| const DISABLED = process.env.TONNODE_DISABLE_WALLET_GEN === "1"; | ||
| // Highload Wallet V3 is not in @ton/ton. Its address is derived from the | ||
| // official contract code (ton-blockchain/highload-wallet-contract-v3) plus a | ||
| // data cell of {publicKey, subwalletId, two empty dicts, last_clean_time=0, | ||
| // timeout}. Both the code BOC and this layout were cross-verified against the | ||
| // maintained @tonkite/highload-wallet-v3 package — the derived address matches | ||
| // byte-for-byte. Unlike seqno wallets, the highload address depends on | ||
| // subwalletId AND timeout, so they are exposed as parameters. | ||
| const HIGHLOAD_V3_CODE_HEX = "b5ee9c7241021001000228000114ff00f4a413f4bcf2c80b01020120020d02014803040078d020d74bc00101c060b0915be101d0d3030171b0915be0fa4030f828c705b39130e0d31f018210ae42e5a4ba9d8040d721d74cf82a01ed55fb04e030020120050a02027306070011adce76a2686b85ffc00201200809001aabb6ed44d0810122d721d70b3f0018aa3bed44d08307d721d70b1f0201200b0c001bb9a6eed44d0810162d721d70b15800e5b8bf2eda2edfb21ab09028409b0ed44d0810120d721f404f404d33fd315d1058e1bf82325a15210b99f326df82305aa0015a112b992306dde923033e2923033e25230800df40f6fa19ed021d721d70a00955f037fdb31e09130e259800df40f6fa19cd001d721d70a00937fdb31e0915be270801f6f2d48308d718d121f900ed44d0d3ffd31ff404f404d33fd315d1f82321a15220b98e12336df82324aa00a112b9926d32de58f82301de541675f910f2a106d0d31fd4d307d30cd309d33fd315d15168baf2a2515abaf2a6f8232aa15250bcf2a304f823bbf2a35304800df40f6fa199d024d721d70a00f2649130e20e01fe5309800df40f6fa18e13d05004d718d20001f264c858cf16cf8301cf168e1030c824cf40cf8384095005a1a514cf40e2f800c94039800df41704c8cbff13cb1ff40012f40012cb3f12cb15c9ed54f80f21d0d30001f265d3020171b0925f03e0fa4001d70b01c000f2a5fa4031fa0031f401fa0031fa00318060d721d300010f0020f265d2000193d431d19130e272b1fb00b585bf03"; | ||
| const HIGHLOAD_DEFAULT_SUBWALLET = 0x10ad; | ||
| const HIGHLOAD_DEFAULT_TIMEOUT = 60 * 60 * 24; // 24h — matches standard tooling | ||
| function highloadAddress(workchain, publicKey, subwalletId, timeout) { | ||
| const code = Cell.fromBoc(Buffer.from(HIGHLOAD_V3_CODE_HEX, "hex"))[0]; | ||
| const data = beginCell() | ||
| .storeBuffer(publicKey) // 256 bits | ||
| .storeUint(subwalletId, 32) | ||
| .storeUint(0, 1 + 1 + 64) // old_queries + queries (empty dicts) + last_clean_time | ||
| .storeUint(timeout, 22) | ||
| .endCell(); | ||
| return contractAddress(workchain, { code, data }); | ||
| } | ||
| function addressFor(version, workchain, publicKey, subwalletId, timeout) { | ||
| switch (version) { | ||
| case "v3r2": | ||
| return WalletContractV3R2.create({ workchain, publicKey }).address; | ||
| case "v4": | ||
| return WalletContractV4.create({ workchain, publicKey }).address; | ||
| case "v5r1": | ||
| return WalletContractV5R1.create({ workchain, publicKey }).address; | ||
| case "highload_v3": | ||
| return highloadAddress(workchain, publicKey, subwalletId, timeout); | ||
| } | ||
| } | ||
| export function registerWalletTools(server) { | ||
| if (DISABLED) | ||
| return; | ||
| server.registerTool("generate_wallet", { | ||
| title: "Generate TON wallet", | ||
| description: "Create a brand-new TON wallet: a fresh 24-word mnemonic, its ed25519 keypair, and the wallet address for the chosen contract version. " + | ||
| "Use when an agent needs its own wallet to receive or send funds (e.g. before build_swap_tx). " + | ||
| "SECURITY — READ BEFORE USE: this returns SECRET key material (mnemonic + private key). Anyone who sees this response controls the wallet and any funds in it. " + | ||
| "In hosted mode the keys are generated on the server and returned over TLS, so treat the wallet as HOT: fine for programmatic/ephemeral use, but move any significant balance to a hardware or cold wallet, and keep this response out of logs and shared transcripts. " + | ||
| "The server does not store or log the mnemonic or private key — only the public address. Losing the mnemonic means losing the funds; there is no recovery. " + | ||
| "Versions: v4 (most common, recommended default), v3r2 (simple/legacy), v5r1 (W5 — gasless-capable, newest), highload_v3 (mass-payout wallet for exchanges/payment systems). " + | ||
| "For highload_v3 the address also depends on subwallet_id and timeout_sec (they are part of the contract data), so changing them changes the address; the defaults match standard tooling — store them ALONGSIDE the mnemonic, as the mnemonic alone cannot reproduce a highload address with non-default params. " + | ||
| "EACH CALL CREATES A NEW, INDEPENDENT WALLET — never re-call this tool to 're-read' a wallet you already made; you will get a different one and orphan any funds sent to the first. " + | ||
| "Returns: address (recommended_deposit_address plus bounceable EQ…, non_bounceable UQ… and raw forms), public_key and private_key (hex), the 24-word mnemonic, plus version and workchain. " + | ||
| "The address is UNINITIALIZED until the wallet is deployed by its first outgoing transaction — receiving funds does not require deployment, but the FIRST deposit must be sent to the non_bounceable (UQ…) address: a bounceable send to an undeployed wallet bounces back to the sender. Use recommended_deposit_address for incoming funds.", | ||
| inputSchema: { | ||
| version: z | ||
| .enum(["v4", "v3r2", "v5r1", "highload_v3"]) | ||
| .default("v4") | ||
| .describe("Wallet contract version: v4 (recommended), v3r2 (legacy), v5r1 (W5, newest), highload_v3 (mass payouts)"), | ||
| workchain: z | ||
| .number() | ||
| .int() | ||
| .refine((w) => w === 0 || w === -1, "workchain must be 0 (basechain) or -1 (masterchain)") | ||
| .default(0) | ||
| .describe("0 = basechain (normal wallets), -1 = masterchain (rarely what you want)"), | ||
| subwallet_id: z | ||
| .number() | ||
| .int() | ||
| .min(0) | ||
| .max(0xffffffff) | ||
| .default(HIGHLOAD_DEFAULT_SUBWALLET) | ||
| .describe("highload_v3 only: subwallet id baked into the address (default 4269 / 0x10ad, the recommended value)"), | ||
| timeout_sec: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(0x3fffff) | ||
| .default(HIGHLOAD_DEFAULT_TIMEOUT) | ||
| .describe("highload_v3 only: message validity window in seconds, baked into the address (default 86400)"), | ||
| }, | ||
| outputSchema: { | ||
| version: z.string(), | ||
| workchain: z.number(), | ||
| recommended_deposit_address: z.string(), | ||
| address: z.object({ | ||
| bounceable: z.string(), | ||
| non_bounceable: z.string(), | ||
| raw: z.string(), | ||
| }), | ||
| public_key: z.string(), | ||
| private_key: z.string(), | ||
| mnemonic: z.array(z.string()), | ||
| subwallet_id: z.number().optional(), | ||
| timeout_sec: z.number().optional(), | ||
| warning: z.string(), | ||
| }, | ||
| // NOT readOnly: although it is pure computation with no external side | ||
| // effect, it MINTS SECRET key material and is non-idempotent (a new wallet | ||
| // each call). readOnlyHint would tell orchestrators it is safe to auto-run | ||
| // and re-run — neither is true for a secret-emitting generator. | ||
| annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, | ||
| }, async ({ version, workchain, subwallet_id, timeout_sec }) => { | ||
| try { | ||
| const isHighload = version === "highload_v3"; | ||
| const mnemonic = await mnemonicNew(); // 24 words, cryptographically secure | ||
| const kp = await mnemonicToPrivateKey(mnemonic); | ||
| const addr = addressFor(version, workchain, kp.publicKey, subwallet_id, timeout_sec); | ||
| // never log the secret — the address is the only thing that goes to the log | ||
| console.error(`wallet: generated ${version} ${addr.toString({ bounceable: false })}`); | ||
| const nonBounceable = addr.toString({ bounceable: false }); | ||
| return ok({ | ||
| version, | ||
| workchain, | ||
| // funding an undeployed wallet must target the non-bounceable form, | ||
| // or a bounceable first transfer bounces back to the sender | ||
| recommended_deposit_address: nonBounceable, | ||
| address: { | ||
| bounceable: addr.toString({ bounceable: true }), | ||
| non_bounceable: nonBounceable, | ||
| raw: addr.toRawString(), | ||
| }, | ||
| public_key: kp.publicKey.toString("hex"), | ||
| private_key: kp.secretKey.toString("hex"), | ||
| mnemonic, | ||
| ...(isHighload ? { subwallet_id, timeout_sec } : {}), | ||
| warning: "SECRET — anyone with this mnemonic or private key controls the wallet. Store the mnemonic securely, keep it out of logs, and move meaningful funds to cold storage. There is no recovery if it is lost." + | ||
| (isHighload | ||
| ? " For highload_v3, store subwallet_id and timeout_sec together with the mnemonic — the address cannot be reproduced from the mnemonic alone." | ||
| : "") + | ||
| " Send the first deposit to recommended_deposit_address (non-bounceable); a bounceable transfer to this still-undeployed wallet bounces back to the sender.", | ||
| }); | ||
| } | ||
| catch (err) { | ||
| return fail(err); | ||
| } | ||
| }); | ||
| } |
+13
-0
@@ -34,2 +34,4 @@ // Cross-chain swaps: TON → EVM chains via Omniston's HTLC escrow settlement | ||
| const DISCLOSE_PROBE_WAIT_MS = 6_000; | ||
| // Refuse disclosure this close (seconds) to the destination rollback opening. | ||
| const ROLLBACK_SAFETY_MARGIN_S = 60; | ||
| const READY_PHASES = new Set([ | ||
@@ -464,2 +466,13 @@ "EXECUTION_PHASE_READY_FOR_PRIVATE_COMPLETION", | ||
| } | ||
| // Refuse to disclose within a safety margin of the destination | ||
| // position's rollback opening: past it the resolver could roll the | ||
| // destination back and still claim the TON side with the now-public | ||
| // preimage — the classic late-reveal loss. Refund instead. (Checked | ||
| // last, after the secret is validated, so a mistyped secret gets the | ||
| // precise error rather than this one.) | ||
| const rollbackAt = exec.outputPositionPhaseTimestamps?.privateRollbackAvailableTimestamp; | ||
| if (rollbackAt && Date.now() / 1000 + ROLLBACK_SAFETY_MARGIN_S > rollbackAt) { | ||
| throw new Error("too close to the destination rollback window — a late reveal risks losing the input; " + | ||
| "do NOT disclose, reclaim the escrow with build_crosschain_refund instead; secret NOT disclosed"); | ||
| } | ||
| await withDeadline(omni.orderDiscloseHtlcSecret({ | ||
@@ -466,0 +479,0 @@ quoteId: id, |
+73
-0
@@ -79,2 +79,68 @@ // Hosted mode: the same TON MCP server over Streamable HTTP. | ||
| } | ||
| /** | ||
| * The caller's address as seen past the TLS proxy. | ||
| * | ||
| * Caddy sets X-Forwarded-For and we bind 127.0.0.1, so the only writer of that | ||
| * header is our own proxy — a client-supplied value cannot reach us. The first | ||
| * entry is the original client. | ||
| */ | ||
| function clientIp(req) { | ||
| const fwd = req.headers["x-forwarded-for"]; | ||
| const raw = Array.isArray(fwd) ? fwd[0] : fwd; | ||
| const first = (raw ?? "").split(",")[0].trim(); | ||
| const addr = first || req.socket.remoteAddress || ""; | ||
| // ::ffff:1.2.3.4 → 1.2.3.4 | ||
| return addr.startsWith("::ffff:") ? addr.slice(7) : addr; | ||
| } | ||
| function ipToBig(ip) { | ||
| if (ip.includes(":")) { | ||
| // IPv6, possibly with a :: run. | ||
| const [head, tail] = ip.split("::"); | ||
| const h = head ? head.split(":") : []; | ||
| const t = tail ? tail.split(":") : []; | ||
| if (h.length + t.length > 8) | ||
| return null; | ||
| const parts = ip.includes("::") | ||
| ? [...h, ...Array(8 - h.length - t.length).fill("0"), ...t] | ||
| : ip.split(":"); | ||
| if (parts.length !== 8) | ||
| return null; | ||
| let out = 0n; | ||
| for (const part of parts) { | ||
| if (!/^[0-9a-f]{0,4}$/i.test(part)) | ||
| return null; | ||
| out = (out << 16n) | BigInt(parseInt(part || "0", 16)); | ||
| } | ||
| return out; | ||
| } | ||
| const octets = ip.split("."); | ||
| if (octets.length !== 4) | ||
| return null; | ||
| let out = 0n; | ||
| for (const octet of octets) { | ||
| const n = Number(octet); | ||
| if (!/^\d{1,3}$/.test(octet) || n > 255) | ||
| return null; | ||
| out = (out << 8n) | BigInt(n); | ||
| } | ||
| return out; | ||
| } | ||
| /** True when `ip` falls inside `rule`, which is an address or a CIDR block. */ | ||
| function ipMatches(ip, rule) { | ||
| const [network, bitsRaw] = rule.split("/"); | ||
| const a = ipToBig(ip); | ||
| const b = ipToBig(network); | ||
| if (a === null || b === null) | ||
| return false; | ||
| if (bitsRaw === undefined) | ||
| return a === b; | ||
| const width = network.includes(":") ? 128 : 32; | ||
| const bits = Number(bitsRaw); | ||
| if (!Number.isInteger(bits) || bits < 0 || bits > width) | ||
| return false; | ||
| if (ip.includes(":") !== network.includes(":")) | ||
| return false; | ||
| const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << BigInt(width - bits); | ||
| return (a & mask) === (b & mask); | ||
| } | ||
| function authenticate(req) { | ||
@@ -100,2 +166,9 @@ if (OPEN_MODE) | ||
| return null; | ||
| if (rec.ips && rec.ips.length > 0) { | ||
| const ip = clientIp(req); | ||
| if (!ip || !rec.ips.some((rule) => ipMatches(ip, rule))) { | ||
| console.error(`auth: ${key.slice(0, 11)}… rejected from ${ip || "unknown"} (not in allowlist)`); | ||
| return null; | ||
| } | ||
| } | ||
| return key; | ||
@@ -102,0 +175,0 @@ } |
+155
-2
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { z } from "zod"; | ||
| import { Address, beginCell, Cell, fromNano, loadTransaction, parseTuple, serializeTuple } from "@ton/core"; | ||
| import { createHash } from "node:crypto"; | ||
| import { Address, beginCell, Cell, Dictionary, fromNano, loadTransaction, parseTuple, serializeTuple } from "@ton/core"; | ||
| import { getClient, withTimeout } from "./lite.js"; | ||
| import { registerSwapTools } from "./swap.js"; | ||
| import { registerCrosschainTools } from "./crosschain.js"; | ||
| import { registerWalletTools } from "./wallet.js"; | ||
| export function ok(data) { | ||
@@ -53,4 +55,46 @@ // strip bigints once; the same plain object feeds both the text block and structuredContent | ||
| .describe("TON address in friendly (EQ…/UQ…) or raw (0:… / -1:…) form"); | ||
| // ---- TEP-64 jetton metadata parsing (for get_jetton_info) ---- | ||
| /** Dictionary key for an on-chain metadata attribute: sha256(name) as a 256-bit int. */ | ||
| function metaKey(name) { | ||
| return BigInt("0x" + createHash("sha256").update(name, "ascii").digest("hex")); | ||
| } | ||
| /** Read a TEP-64 "snake" string: the bytes in this slice, continued in ref[0]. */ | ||
| function readSnake(slice) { | ||
| const chunks = []; | ||
| let s = slice; | ||
| for (let depth = 0; depth < 64; depth++) { | ||
| const bytes = Math.floor(s.remainingBits / 8); | ||
| if (bytes > 0) | ||
| chunks.push(s.loadBuffer(bytes)); | ||
| if (s.remainingRefs > 0) | ||
| s = s.loadRef().beginParse(); | ||
| else | ||
| break; | ||
| } | ||
| return Buffer.concat(chunks); | ||
| } | ||
| /** Decode an on-chain metadata value cell (0x00 snake / 0x01 chunked) to a string. */ | ||
| function decodeMetaValue(cell) { | ||
| try { | ||
| const s = cell.beginParse(); | ||
| if (s.remainingBits < 8) | ||
| return null; | ||
| const tag = s.loadUint(8); | ||
| if (tag === 0x00) | ||
| return readSnake(s).toString("utf-8"); | ||
| if (tag === 0x01) { | ||
| const dict = s.loadDict(Dictionary.Keys.Uint(32), Dictionary.Values.Cell()); | ||
| const parts = [...dict.keys()] | ||
| .sort((a, b) => a - b) | ||
| .map((k) => readSnake(dict.get(k).beginParse())); | ||
| return Buffer.concat(parts).toString("utf-8"); | ||
| } | ||
| return null; | ||
| } | ||
| catch { | ||
| return null; | ||
| } | ||
| } | ||
| export function createTonServer() { | ||
| const server = new McpServer({ name: "tonnode", version: "0.7.0" }); | ||
| const server = new McpServer({ name: "tonnode", version: "0.9.0" }); | ||
| server.registerTool("get_masterchain_info", { | ||
@@ -354,2 +398,110 @@ title: "Masterchain info", | ||
| }); | ||
| server.registerTool("get_jetton_info", { | ||
| title: "Jetton info / metadata", | ||
| description: "Metadata of a jetton master (token) contract: name, symbol, DECIMALS, total supply, mintable, admin. " + | ||
| "Use when: you need a token's decimals to convert raw indivisible units — swap quotes and balances are in raw units, so a human amount = raw / 10^decimals (USDT is 6, most jettons 9). Call this before get_swap_quote/build_swap_tx when you don't know the token's decimals. " + | ||
| "Args: jetton_master — the token's master contract address (e.g. USDT \"EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs\"). " + | ||
| "Returns: name, symbol, decimals (a number, or null when the token stores metadata off-chain), description, total_supply (raw), mintable, admin, metadata_type (onchain/offchain) and metadata_uri. " + | ||
| "Off-chain tokens keep name/symbol/decimals in a JSON file at metadata_uri — this tool returns the URI but does not fetch it, so decimals may be null (assume 6 for USDT-like, 9 otherwise, or fetch the URI).", | ||
| inputSchema: { | ||
| jetton_master: z | ||
| .string() | ||
| .describe('Jetton master contract address, e.g. USDT "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"'), | ||
| }, | ||
| outputSchema: { | ||
| jetton_master: z.string(), | ||
| name: z.string().nullable(), | ||
| symbol: z.string().nullable(), | ||
| decimals: z.number().nullable(), | ||
| description: z.string().nullable(), | ||
| total_supply: z.string(), | ||
| mintable: z.boolean(), | ||
| admin: z.string().nullable(), | ||
| metadata_type: z.string(), | ||
| metadata_uri: z.string().nullable(), | ||
| at_seqno: z.number(), | ||
| }, | ||
| annotations: READ_ONLY, | ||
| }, async ({ jetton_master }) => { | ||
| try { | ||
| const client = await getClient(); | ||
| const master = parseAddress(jetton_master); | ||
| const head = await withTimeout(client.getMasterchainInfo()); | ||
| const res = await withTimeout(client.runMethod(master, "get_jetton_data", serializeTuple([]).toBoc(), head.last)); | ||
| if (res.exitCode !== 0 && res.exitCode !== 1) { | ||
| throw new Error(`get_jetton_data failed with exit code ${res.exitCode} — is ${master.toString()} really a jetton master?`); | ||
| } | ||
| const raw = typeof res.result === "string" ? Buffer.from(res.result, "base64") : res.result; | ||
| const stack = parseTuple(Cell.fromBoc(raw)[0]); | ||
| // get_jetton_data returns: total_supply, mintable, admin, content, wallet_code | ||
| const totalSupply = stack[0]?.type === "int" ? stack[0].value : 0n; | ||
| const mintable = stack[1]?.type === "int" ? stack[1].value !== 0n : false; | ||
| let admin = null; | ||
| const adminItem = stack[2]; | ||
| if (adminItem && (adminItem.type === "slice" || adminItem.type === "cell")) { | ||
| try { | ||
| const a = adminItem.cell.beginParse().loadAddressAny(); | ||
| admin = a instanceof Address ? a.toString() : null; | ||
| } | ||
| catch { | ||
| // addr_none or unparseable — leave null | ||
| } | ||
| } | ||
| let name = null; | ||
| let symbol = null; | ||
| let description = null; | ||
| let decimals = null; | ||
| let metadataType = "unknown"; | ||
| let metadataUri = null; | ||
| const contentItem = stack[3]; | ||
| if (contentItem && (contentItem.type === "cell" || contentItem.type === "slice")) { | ||
| const cs = contentItem.cell.beginParse(); | ||
| const prefix = cs.remainingBits >= 8 ? cs.loadUint(8) : -1; | ||
| if (prefix === 0x01) { | ||
| metadataType = "offchain"; | ||
| metadataUri = readSnake(cs).toString("utf-8") || null; | ||
| } | ||
| else if (prefix === 0x00) { | ||
| metadataType = "onchain"; | ||
| const dict = cs.loadDict(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell()); | ||
| const attr = (k) => { | ||
| const c = dict.get(metaKey(k)); | ||
| return c ? decodeMetaValue(c) : null; | ||
| }; | ||
| name = attr("name"); | ||
| symbol = attr("symbol"); | ||
| description = attr("description"); | ||
| const dec = attr("decimals"); | ||
| if (dec !== null) { | ||
| const n = Number(dec.trim()); | ||
| if (Number.isInteger(n) && n >= 0 && n <= 255) | ||
| decimals = n; | ||
| } | ||
| // semi-chained tokens (e.g. mainnet USDT) keep decimals on-chain but | ||
| // name/symbol/image in an off-chain JSON at a "uri" key — surface it | ||
| const uri = attr("uri"); | ||
| if (uri) { | ||
| metadataUri = uri.trim() || null; | ||
| metadataType = "onchain+uri"; | ||
| } | ||
| } | ||
| } | ||
| return ok({ | ||
| jetton_master: master.toString(), | ||
| name, | ||
| symbol, | ||
| decimals, | ||
| description, | ||
| total_supply: totalSupply.toString(), | ||
| mintable, | ||
| admin, | ||
| metadata_type: metadataType, | ||
| metadata_uri: metadataUri, | ||
| at_seqno: head.last.seqno, | ||
| }); | ||
| } | ||
| catch (err) { | ||
| return fail(err); | ||
| } | ||
| }); | ||
| server.registerTool("parse_address", { | ||
@@ -405,3 +557,4 @@ title: "Parse address", | ||
| registerCrosschainTools(server); | ||
| registerWalletTools(server); | ||
| return server; | ||
| } |
+4
-2
| { | ||
| "name": "@tonnode/mcp", | ||
| "mcpName": "io.github.tonnode/mcp", | ||
| "version": "0.7.0", | ||
| "description": "Liteserver access to TON: balances, account state, transactions, get-methods over native ADNL — plus non-custodial DEX swaps and cross-chain swaps via Omniston.", | ||
| "version": "0.9.0", | ||
| "description": "Liteserver access to TON: balances, account state, transactions, get-methods over native ADNL — plus non-custodial DEX swaps, cross-chain swaps, and TON wallet generation.", | ||
| "type": "module", | ||
@@ -58,2 +58,4 @@ "bin": { | ||
| "@ton/core": "^0.63.1", | ||
| "@ton/crypto": "^3.3.0", | ||
| "@ton/ton": "^16.3.0", | ||
| "ton-lite-client": "^3.1.1", | ||
@@ -60,0 +62,0 @@ "zod": "^4.4.3" |
+9
-0
@@ -36,2 +36,3 @@ # @tonnode/mcp | ||
| | `get_jetton_balance` | Jetton/token balance (USDT and any TEP-74 token) | *"How much USDT is on this wallet?"* | | ||
| | `get_jetton_info` | Token metadata: name, symbol, **decimals**, supply | *"What are this jetton's decimals?"* | | ||
| | `get_transactions` | Recent transactions: values, senders, fees | *"Did my payment arrive?"* | | ||
@@ -49,3 +50,10 @@ | `get_account_state` | Status, deployment flags, last-tx pointer | *"Is this contract deployed?"* | | ||
| | `build_crosschain_refund` | Unsigned cancellation that reclaims escrowed funds | *"The trade stalled — get my money back"* | | ||
| | `generate_wallet` | Mint a fresh TON wallet (mnemonic + keys + address) | *"Create a wallet for my agent to use"* | | ||
| ## Wallet generation | ||
| `generate_wallet` mints a brand-new wallet — a 24-word mnemonic, its ed25519 keypair and the address for the chosen contract version (**v4** default, plus **v3r2**, **v5r1** and **highload_v3** for mass payouts). The v3r2/v4/v5r1 addresses come from `@ton/ton`'s canonical contracts; highload_v3 is derived from the official contract code and cross-checked against a maintained reference implementation. | ||
| > ⚠️ **This returns secret key material.** In hosted mode the keys are generated on the server and returned over TLS — treat every generated wallet as **hot**: fine for programmatic/ephemeral use, but move any meaningful balance to cold storage, and keep the response out of logs and shared transcripts. The server never stores or logs the mnemonic or private key (only the public address). Operators can set `TONNODE_DISABLE_WALLET_GEN=1` to remove the tool entirely. | ||
| ## Swaps — agents that can actually trade | ||
@@ -110,2 +118,3 @@ | ||
| | `OMNISTON_INTEGRATOR_ADDRESS` / `OMNISTON_INTEGRATOR_FEE_BPS` | Swap tools: optional integrator revenue share in bps of the output — always visible to the caller as `integrator_fee_units` in every quote (default off) | | ||
| | `TONNODE_DISABLE_WALLET_GEN` | Set to `1` to remove the `generate_wallet` tool (e.g. on a shared hosted endpoint where you don't want key material generated server-side) | | ||
@@ -112,0 +121,0 @@ ### A note on public liteservers |
+9
-0
@@ -36,2 +36,3 @@ # @tonnode/mcp | ||
| | `get_jetton_balance` | Баланс жетона/токена (USDT и любой TEP-74) | *«Сколько USDT на этом кошельке?»* | | ||
| | `get_jetton_info` | Метаданные токена: имя, символ, **decimals**, эмиссия | *«Сколько decimals у этого жетона?»* | | ||
| | `get_transactions` | Последние транзакции: суммы, отправители, комиссии | *«Пришёл ли мой платёж?»* | | ||
@@ -49,3 +50,10 @@ | `get_account_state` | Статус, флаги деплоя, указатель последней транзакции | *«Этот контракт задеплоен?»* | | ||
| | `build_crosschain_refund` | Неподписанная отмена, возвращающая средства из эскроу | *«Сделка зависла — верни деньги»* | | ||
| | `generate_wallet` | Создать новый TON-кошелёк (мнемоника + ключи + адрес) | *«Создай кошелёк для моего агента»* | | ||
| ## Генерация кошелька | ||
| `generate_wallet` создаёт новый кошелёк — мнемонику из 24 слов, ed25519-пару ключей и адрес для выбранной версии контракта (**v4** по умолчанию, а также **v3r2**, **v5r1** и **highload_v3** для массовых выплат). Адреса v3r2/v4/v5r1 берутся из канонических контрактов `@ton/ton`; highload_v3 выводится из официального кода контракта и сверен с поддерживаемой референс-реализацией. | ||
| > ⚠️ **Инструмент возвращает секретный ключевой материал.** В hosted-режиме ключи генерируются на сервере и передаются по TLS — считай любой такой кошелёк **горячим**: годится для программного/временного использования, но крупные суммы переводи в холодное хранилище, а ответ держи вне логов и общих переписок. Сервер никогда не сохраняет и не логирует мнемонику или приватный ключ (только публичный адрес). Оператор может задать `TONNODE_DISABLE_WALLET_GEN=1`, чтобы полностью убрать инструмент. | ||
| ## Свапы — агенты, которые умеют торговать | ||
@@ -110,2 +118,3 @@ | ||
| | `OMNISTON_INTEGRATOR_ADDRESS` / `OMNISTON_INTEGRATOR_FEE_BPS` | Свапы: необязательная интеграторская комиссия в bps от выхода — всегда видна вызывающему как `integrator_fee_units` в каждой котировке (по умолчанию выключена) | | ||
| | `TONNODE_DISABLE_WALLET_GEN` | Задай `1`, чтобы убрать инструмент `generate_wallet` (например, на общем hosted-эндпоинте, где не нужно генерировать ключи на сервере) | | ||
@@ -112,0 +121,0 @@ ### О публичных лайтсерверах |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
134164
22.74%12
9.09%2104
22.75%127
7.63%8
33.33%22
10%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added