import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { createArcPaymentClient, getClientPrivateKey } from "./arc.js"; type ArcPaymentClient = ReturnType; interface PaidCaptureResult { title?: string; url?: string; capturedAt?: string; captureTimeMs?: number; payment?: { price?: string; network?: string; }; refund?: { amount?: string; reason?: string; transaction?: string; }; content?: string; } const EVM_PRIVATE_KEY = getClientPrivateKey(); const SERVER_URL = process.env.EYEZ_URL || `http://localhost:${process.env.PORT || 3001}`; let httpClient: ArcPaymentClient["httpClient"] | null = null; let client: ArcPaymentClient["client"] | null = null; function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function getPaymentClient(): { client: ArcPaymentClient["client"] | null; httpClient: ArcPaymentClient["httpClient"] | null; } { if (!client && EVM_PRIVATE_KEY) { const paymentClient = createArcPaymentClient(EVM_PRIVATE_KEY); client = paymentClient.client; httpClient = paymentClient.httpClient; } return { client, httpClient }; } async function paidCapture(url: string): Promise { const { client, httpClient } = getPaymentClient(); if (!client || !httpClient) { throw new Error( "EVM_PRIVATE_KEY or ARC_PRIVATE_KEY not set - cannot make x402 payments", ); } const captureEndpoint = `${SERVER_URL}/capture?url=${encodeURIComponent(url)}`; const firstTry = await fetch(captureEndpoint); if (firstTry.status !== 402) { return (await firstTry.json()) as PaidCaptureResult; } const paymentRequired = httpClient.getPaymentRequiredResponse((name) => firstTry.headers.get(name), ); const paymentPayload = await client.createPaymentPayload(paymentRequired); const headers = httpClient.encodePaymentSignatureHeader(paymentPayload); const resp = await fetch(captureEndpoint, { method: "GET", headers }); if (!resp.ok) { const text = await resp.text(); throw new Error(`Capture failed (${resp.status}): ${text}`); } return (await resp.json()) as PaidCaptureResult; } export function createEyezMcpServer(): McpServer { const server = new McpServer({ name: "eyez", version: "1.0.0", }); server.tool( "capturePage", "Capture a JavaScript-heavy webpage using a headless browser. Pays $0.001 USDC on Arc 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 capture (must be public http/https)"), }, async ({ url }) => { try { const result = await paidCapture(url); return { content: [ { type: "text", text: `# ${result.title}\n\nURL: ${result.url}\nCaptured: ${result.capturedAt}\nCapture time: ${result.captureTimeMs}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: ${getErrorMessage(err)}` }], isError: true, }; } }, ); return server; }