#!/usr/bin/env node import "dotenv/config"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { Transaction, TransactionBuilder } from "@stellar/stellar-sdk"; import { x402Client, x402HTTPClient } from "@x402/fetch"; import { createEd25519Signer, getNetworkPassphrase } from "@x402/stellar"; import { ExactStellarScheme } from "@x402/stellar/exact/client"; const DEMO_PRIVATE_KEY = process.env.DEMO_PRIVATE_KEY; const SERVER_URL = process.env.RENDERGATE_URL || "https://tantk-rendergate.hf.space"; const NETWORK = "stellar:testnet"; const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; // Setup x402 payment client let httpClient = null; let client = null; function getPaymentClient() { if (!client && DEMO_PRIVATE_KEY) { const signer = createEd25519Signer(DEMO_PRIVATE_KEY, NETWORK); client = new x402Client().register( "stellar:*", new ExactStellarScheme(signer, { url: STELLAR_RPC_URL }), ); httpClient = new x402HTTPClient(client); } return { client, httpClient }; } async function paidRender(url) { const { client, httpClient } = getPaymentClient(); if (!client) { throw new Error("DEMO_PRIVATE_KEY not set — cannot make x402 payments"); } const renderEndpoint = `${SERVER_URL}/render?url=${encodeURIComponent(url)}`; // Step 1: Get 402 response const firstTry = await fetch(renderEndpoint); if (firstTry.status !== 402) { return await firstTry.json(); } // Step 2: Create payment const paymentRequired = httpClient.getPaymentRequiredResponse((name) => firstTry.headers.get(name), ); let paymentPayload = await client.createPaymentPayload(paymentRequired); const networkPassphrase = getNetworkPassphrase(NETWORK); const tx = new Transaction( paymentPayload.payload.transaction, networkPassphrase, ); const sorobanData = tx.toEnvelope().v1()?.tx()?.ext()?.sorobanData(); if (sorobanData) { paymentPayload = { ...paymentPayload, payload: { ...paymentPayload.payload, transaction: TransactionBuilder.cloneFrom(tx, { fee: "1", sorobanData, networkPassphrase, }) .build() .toXDR(), }, }; } // Step 3: Send paid request const headers = httpClient.encodePaymentSignatureHeader(paymentPayload); const resp = await fetch(renderEndpoint, { method: "GET", headers }); if (!resp.ok) { const text = await resp.text(); throw new Error(`Render failed (${resp.status}): ${text}`); } return await resp.json(); } // Create MCP server const server = new McpServer({ name: "rendergate", version: "1.0.0", }); server.tool( "render_page", "Render a JavaScript-heavy webpage using a headless browser. Pays $0.001 USDC on Stellar per request via x402. Use this when standard HTTP fetch returns empty content from SPAs, Twitter/X, LinkedIn, DeFi apps, or Cloudflare-protected sites.", { url: z.string().url().describe("The URL to render (must be public http/https)"), }, async ({ url }) => { try { const result = await paidRender(url); return { content: [ { type: "text", text: `# ${result.title}\n\nURL: ${result.url}\nRendered: ${result.renderedAt}\nRender time: ${result.renderTimeMs}ms\nPayment: ${result.payment?.price} on ${result.payment?.network}${result.refund ? `\nRefund: ${result.refund.amount} — ${result.refund.reason} (tx: ${result.refund.transaction})` : ""}\n\n${result.content}`, }, ], }; } catch (err) { return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true, }; } }, ); // Start const transport = new StdioServerTransport(); await server.connect(transport);