branch:
server.ts
3530 bytesRaw
import { Agent, callable, routeAgentRequest } from "agents";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import {
  withX402,
  withX402Client,
  type X402Config,
  type PaymentRequirements
} from "agents/x402";
import { z } from "zod";
import { toClientEvmSigner } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";

// --- MCP Server with paid tools ---

const X402_CONFIG: X402Config = {
  network: "eip155:84532",
  recipient: process.env.MCP_ADDRESS as `0x${string}`,
  facilitator: { url: "https://x402.org/facilitator" }
};

export class PayMCP extends McpAgent<Env> {
  server = withX402(
    new McpServer({ name: "PayMCP", version: "1.0.0" }),
    X402_CONFIG
  );

  async init() {
    this.server.paidTool(
      "square",
      "Squares a number",
      0.01,
      { number: z.number() },
      {},
      async ({ number }) => ({
        content: [{ type: "text", text: String(number ** 2) }]
      })
    );

    this.server.registerTool(
      "echo",
      {
        description: "Echo a message",
        inputSchema: { message: z.string() }
      },
      async ({ message }) => ({
        content: [{ type: "text", text: message }]
      })
    );
  }
}

// --- Client agent that calls paid tools ---

export class PayAgent extends Agent<Env> {
  pendingPayments: Record<string, (confirmed: boolean) => void> = {};
  x402Client?: ReturnType<typeof withX402Client>;

  async onStart() {
    const pk = process.env.CLIENT_TEST_PK;
    if (!pk) {
      console.warn(
        "CLIENT_TEST_PK not set — copy .env.example to .env and add a test private key"
      );
      return;
    }

    const account = privateKeyToAccount(pk as `0x${string}`);

    const { id } = await this.addMcpServer("pay-mcp", this.env.PayMCP);

    this.x402Client = withX402Client(this.mcp.mcpConnections[id].client, {
      network: "eip155:84532",
      account: toClientEvmSigner(account)
    });
  }

  private async requestPaymentConfirmation(
    requirements: PaymentRequirements[]
  ): Promise<boolean> {
    const confirmationId = crypto.randomUUID().slice(0, 4);

    this.broadcast(
      JSON.stringify({
        type: "payment_required",
        confirmationId,
        requirements
      })
    );

    return new Promise<boolean>((resolve) => {
      this.pendingPayments[confirmationId] = resolve;
    });
  }

  @callable()
  async callTool(toolName: string, args: Record<string, unknown>) {
    if (!this.x402Client) {
      return { text: "Agent not ready", isError: true };
    }

    const res = await this.x402Client.callTool(
      this.requestPaymentConfirmation.bind(this),
      { name: toolName, arguments: args }
    );

    return {
      text:
        res.content[0]?.type === "text"
          ? (res.content[0]?.text ?? "")
          : JSON.stringify(res.content),
      isError: res.isError ?? false
    };
  }

  @callable()
  resolvePayment(confirmationId: string, confirmed: boolean) {
    this.pendingPayments[confirmationId]?.(confirmed);
    delete this.pendingPayments[confirmationId];
  }
}

// --- Routing ---

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const url = new URL(request.url);

    if (url.pathname === "/mcp") {
      return PayMCP.serve("/mcp", { binding: "PayMCP" }).fetch(
        request,
        env,
        ctx
      );
    }

    return (
      (await routeAgentRequest(request, env)) ||
      new Response(null, { status: 404 })
    );
  }
};