DEV Community

Cover image for Developing and Deploying an x402 MCP Server to Cloudflare Workers using VibeKanban!
Haruki Kondo
Haruki Kondo

Posted on

Developing and Deploying an x402 MCP Server to Cloudflare Workers using VibeKanban!

Introduction

Hello everyone!

I recently properly studied Cloudflare Workers for the first time, so I'm writing this article to share my findings!

This post will cover what I tried during implementation and how to deploy an MCP server to Cloudflare Workers!

What is Cloudflare Workers?

Cloudflare Workers is a serverless computing platform provided by Cloudflare.

While there are some constraints like bundle size, its charm lies in the ease of deploying TypeScript/JavaScript apps with a frontend-like feel! It also integrates seamlessly with other major Cloudflare services like KV and D1.

Great Compatibility with Hono

Hono is a lightweight, fast, and modern web framework for developing web applications and APIs, primarily in TypeScript/JavaScript. Being fast and lightweight, it is extremely compatible with Cloudflare Workers!

What is Vibe Kanban?

Vibe Kanban - Orchestrate AI Coding Agents

Get the most out of coding agents like Claude Code, Gemini CLI and Amp. Orchestrate multiple AI coding agents, track tasks, and manage your development workflow efficiently.

favicon vibekanban.com

Vibe Kanban is a tool that manages AI coding agents (like Claude Code or Codex) in a Kanban format (task visualization) to realize automated development flows!

Development Workflow with cc-sdd + VibeKanban + GitWorkTree

I practiced the following development workflow for this implementation:

  • 0. Formulate product vision and concept.
  • 1. Create requirements, design documents, and task lists with cc-sdd.
  • 2. Register tasks in VibeKanban (register as GitHub Issues).

  • 3. Prepare working directories with git worktree.

  • 4. Parallel execution of tasks.

  • 5. Self-review deliverables and create PRs.

I used CodeRabbit for code reviews!

Converting and Registering cc-sdd Tasks to VibeKanban

I used a prompt like this to convert tasks generated by cc-sdd into VibeKanban tasks:

Review the task list generated by @(cc-sdd) and register the work plan as Tasks in vibe_kanban.

Each task should include:
- Refer to @design.md for design details.
- Specific work content for this task.
- Dependencies on other tasks or if parallel work is possible.
- If it can be executed in parallel, add a + to the title.

Register tasks in descending order.
Enter fullscreen mode Exit fullscreen mode

The best part is being able to turn them directly into GitHub Issues!

What is x402?

x402 is a standard protocol for stablecoin payments announced by Coinbase, a prominent cryptocurrency exchange in the US.

True to its name, it adopts the HTTP status code 402 Payment Required and has gained significant attention for its compliance with the HTTP protocol.

Cloudflare has not only co-founded the x402 Foundation but, given its compatibility with AI agents, it's a technology that has garnered immense interest within various blockchain tech stacks.

Launching the x402 Foundation with Coinbase, and support for x402 transactions

Cloudflare is partnering with Coinbase to create the x402 Foundation and adding x402 support to the Agents SDK & MCP Servers.

favicon blog.cloudflare.com

About the App I Built

Overview

I developed and deployed an x402 backend server and an MCP server on Cloudflare Workers. I created a sample app where stablecoin payments are processed simultaneously when weather information is retrieved through a chat interface within a GPT App!

What it can do:

  • Call the get_weather tool from a GPT App to retrieve weather information.
  • Access /weather only for requests that have passed x402 payment verification.
  • Deploy x402server and mcpserver separately on Cloudflare Workers.
  • Verify the integrated operation of mcpserver -> x402FetchClient -> x402server via E2E tests.

Sample Code

The source code for this project is available in the following GitHub repository:

vibekanban-gitworktree-sample

VibeKanbanとGitWorktreeを掛け合わせたサンプルアプリ

概要

x402バックエンドサーバーとMCPサーバーを使ってGPT App内のチャットインターフェースから天気予報の情報を取得すると同時にステーブルコイン支払いが行われるサンプルアプリ。

このプロジェクトでできること

  • GPT App から get_weather ツールを呼び出して天気情報を取得
  • x402 による支払い検証を通過したリクエストのみ /weather にアクセス
  • Cloudflare Workers 上で x402servermcpserver を分離デプロイ
  • E2E テストで mcpserver -> x402FetchClient -> x402server の結合動作を検証

構成

リポジトリ構成

パス 役割 主な技術
pkgs/x402server 天気 API と x402 決済検証を提供するバックエンド Hono / x402 / Cloudflare Workers / TypeScript
pkgs/mcpserver GPT App から呼び出される MCP サーバー。x402server を決済付きで呼び出す Hono MCP / MCP SDK / x402 fetch / Cloudflare Workers / TypeScript
pkgs/*/__tests__ 単体・結合テスト群 Vitest
ルート (package.json) monorepo の共通スクリプト・ワークスペース管理 pnpm workspace

リクエストの流れ

  1. GPT App から MCP ツール get_weather を実行
  2. mcpserverx402-fetch-client/weather を呼び出し
  3. x402serverpaymentMiddleware で支払いを検証
  4. 検証後に天気データを返却

機能一覧

機能 概要 提供パッケージ 補足
ヘルスチェック API サービス稼働確認 (/, /health) x402server, mcpserver 監視・疎通確認に利用
天気情報取得 API 都市名を受けて天気を返却 (/weather) x402server 都市未登録時は 404
x402 課金付きアクセス制御 /weather を課金保護し未決済時は 402 を返却 x402server 価格・ネットワークは環境変数で設定
MCP ツール公開 get_weather ツールを外部クライアントへ公開 mcpserver 入力検証・エラー整形を実施
x402 決済付き fetch クライアント 支払い情報付きで x402server を呼び出す mcpserver Service Binding

Repository Structure

Path Role Key Technologies
pkgs/x402server Backend providing Weather API and x402 payment verification. Hono / x402 / Cloudflare Workers / TypeScript
pkgs/mcpserver MCP server called by GPT App. Calls x402server with payment. Hono MCP / MCP SDK / x402 fetch / Cloudflare Workers / TypeScript
Root (package.json) Common scripts and workspace management for the monorepo. pnpm workspace

List of Implemented Features

Feature Overview Provided Package Notes
Health Check API Service availability check (/, /health). x402server, mcpserver Used for monitoring and connectivity checks.
Weather Info API Returns weather based on city name (/weather). x402server Returns 404 if city is not registered.
x402 Paid Access Control Protects /weather and returns 402 if unpaid. x402server Price and network configured via env vars.
MCP Tool Exposure Exposes get_weather tool to external clients. mcpserver Performs input validation and error formatting.
x402 Payment-enabled Fetch Client Calls x402server with payment information. mcpserver Supports both Service Binding and URLs.
E2E Integration Test Verifies flow from MCP to backend. mcpserver tests Confirms 402/404/Success cases.

Key Implementation Points

Let's pick up and introduce some important implementation parts.

x402 Server

First, the x402 server! You need to set environment variables in wrangler.jsonc. Since there's no highly sensitive information here, I'm not using the Secret feature for these.

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "x402server",
  "main": "src/index.ts",
  "compatibility_date": "2026-02-23",
  "compatibility_flags": ["nodejs_compat"],
  "vars": {
    "SERVER_WALLET_ADDRESS": "0x51908F598A5e0d8F1A3bAbFa6DF76F9704daD072",
    "FACILITATOR_URL": "https://x402.org/facilitator",
    "X402_PRICE_USD": "$0.01",
    "X402_NETWORK": "eip155:84532"
  }
}
Enter fullscreen mode Exit fullscreen mode

The x402 server is built on Hono. The main code is in src/app.ts. x402 middleware is applied only to specific routes.

import { paymentMiddleware } from "@x402/hono";
import { Hono } from "hono";
import { createRoutes } from "./route";
import { createResourceServer, resolvePaymentOptions } from "./utils/config";
import type { CreateAppOptions, ErrorResponse, WeatherService } from "./utils/types";
import { createMockWeatherService } from "./weather/service";

const toErrorResponse = (statusCode: number, message: string): ErrorResponse => ({
  statusCode,
  message,
});

export const createApp = (
  weatherService: WeatherService = createMockWeatherService(),
  options: CreateAppOptions = {},
): Hono => {
  const app = new Hono();
  const enablePayment = options.enablePayment ?? true;

  if (enablePayment) {
    const paymentOptions = resolvePaymentOptions(options.payment);
    const resourceServer = createResourceServer(paymentOptions);
    const routes = createRoutes(paymentOptions);
    const protectedRouteKeys = new Set(Object.keys(routes));
    let resourceServerInitialization: Promise<void> | null = null;

    app.use(async (c, next) => {
      const routeKey = `${c.req.method.toUpperCase()} ${c.req.path}`;
      if (!protectedRouteKeys.has(routeKey)) return next();

      if (!resourceServerInitialization) {
        resourceServerInitialization = resourceServer.initialize().catch((error) => {
          resourceServerInitialization = null;
          throw error;
        });
      }
      await resourceServerInitialization;
      return next();
    });

    app.use(paymentMiddleware(routes, resourceServer, undefined, undefined, false));
  }

  app.get("/", (c) => c.json({ status: "ok" }, 200));
  app.get("/health", (c) => c.json({ status: "ok" }, 200));

  app.get("/weather", async (c) => {
    const city = c.req.query("city")?.trim();
    if (!city) return c.json(toErrorResponse(400, "city query parameter is required"), 400);

    try {
      const weather = await weatherService.getWeatherByCity(city);
      if (!weather) return c.json(toErrorResponse(404, "city not found"), 404);
      return c.json(weather, 200);
    } catch {
      return c.json(toErrorResponse(503, "weather service unavailable"), 503);
    }
  });

  return app;
};
Enter fullscreen mode Exit fullscreen mode

Regarding the weather retrieval logic: for verification purposes, I've implemented it to return hardcoded demo data instead of hitting an external API. In a production setting, this would be where you call an external service.

import { WeatherData, WeatherService } from "../utils/types";

const MOCK_WEATHER_DATA: ReadonlyArray<WeatherData> = [
  { city: "Tokyo", condition: "Sunny", temperatureC: 28, humidity: 60 },
  { city: "Osaka", condition: "Cloudy", temperatureC: 26, humidity: 65 },
  { city: "New York", condition: "Rainy", temperatureC: 22, humidity: 72 },
];

const normalizeCity = (city: string): string => {
  const trimmed = city.trim().replace(/^['\"]+|['\"]+$/g, "");
  const withoutCountry = trimmed.split(",")[0]?.trim() ?? trimmed;
  return withoutCountry.toLowerCase();
};

export const createMockWeatherService = (): WeatherService => {
  return {
    async getWeatherByCity(city: string): Promise<WeatherData | null> {
      const normalized = normalizeCity(city);
      return MOCK_WEATHER_DATA.find((item) => normalizeCity(item.city) === normalized) ?? null;
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

x402-specific settings are consolidated in src/utils/config.ts. Implementing an x402 server requires configuring a facilitator and a resource server.

Briefly, a facilitator acts as a bridge between the API you want to apply x402 to and the blockchain, handling signature verification and transaction submission for payments. Facilitators are recommended to save development effort.

MCP Server

The MCP server is also based on Hono.

import { StreamableHTTPTransport } from "@hono/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Hono } from "hono";
import { cors } from "hono/cors";
// ... (imports)

export const createApp = (options: CreateAppOptions = {}): Hono => {
  const app = new Hono();
  const mcpServer = new McpServer({ name: "x402-weather-payment-mcpserver", version: "1.0.0" });
  // ... (logic to handle MCP via Streamable HTTP transport)
  return app;
};
Enter fullscreen mode Exit fullscreen mode

The x402 client setup is implemented in x402-fetch-client.ts.

// ... (X402FetchClient implementation using wrapFetch from @x402/fetch)
Enter fullscreen mode Exit fullscreen mode

And the tool for retrieving weather information:

// ... (get_weather tool registration using McpServer.registerTool)
Enter fullscreen mode Exit fullscreen mode

The Point I Got Stuck On

I learned this for the first time: when calling one Worker from another, you cannot simply specify the URL; you must use Bindings. You need to register this in your wrangler.jsonc.

{
  "services": [
    {
      "binding": "X402SERVER",
      "service": "x402server"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

How to Deploy to Cloudflare Workers!

Now that the code is explained, let's look at the deployment!

Setup

Install dependencies:

pnpm i
Enter fullscreen mode Exit fullscreen mode

x402 Backend Server

Deploy:

pnpm x402server run deploy
Enter fullscreen mode Exit fullscreen mode

MCP Server

Condition: x402server must already be deployed! Register the private key for the x402 client and the x402server endpoint using Secrets.

pnpm mcpserver run secret CLIENT_PRIVATE_KEY --name mcpserver
pnpm mcpserver run secret X402_SERVER_URL --name mcpserver
Enter fullscreen mode Exit fullscreen mode

Deploy:

pnpm mcpserver run deploy
Enter fullscreen mode Exit fullscreen mode

Register https://mcpserver.<unique-id>.workers.dev/mcp as the GPT App URL to enable x402 payments from chat!

How to Call from Within ChatGPT

  1. Register the MCP server endpoint in your GPT App.
  2. Add the app from the + button and ask for the weather.
  3. If the weather is returned and USDC payment is processed, you're set!

Check the block explorer to confirm the stablecoin payment!

Summary

That's it for now! While you can achieve similar results using AWS Lambda or AgentCore, Cloudflare Workers is highly recommended for quick and easy experimentation. The sample code for x402 is abundant and fits perfectly!

Thank you for reading until the end!

References

Top comments (0)