👋

MCPサーバーを Cloudflare Workers にデプロイしてスクリーンショット機能を実装する

に公開

Model Context Protocol (MCP) は、AIアシスタントが外部システムとやり取りするための標準プロトコルです。本記事では、MCPサーバーを Cloudflare Workers にデプロイし、任意の URL のスクリーンショットを撮影できるツールを実装する方法を解説していきます。

技術スタックの選定

今回の実装では、Hono という軽量な Web フレームワークを使用します。実行環境には Cloudflare Workers を採用し、サーバーレスアーキテクチャで構築していきます。状態管理には Durable Objects を使い、スクリーンショット撮影には Browser Rendering API を活用するという構成になっています。

また、ツールの使用状況を追跡するために Analytics Engine を導入し、TypeScript で型安全な開発を行うことで、保守性の高いコードベースを実現していきます。

プロジェクト構成

mcp-hono/
├── src/
│   ├── index.tsx          # メインアプリケーション
│   ├── mcp.ts            # MCPサーバー実装
│   ├── renderer.tsx      # JSXレンダラー
│   └── style.css         # スタイル
├── package.json
├── wrangler.jsonc        # Cloudflare Workers設定
├── vite.config.ts        # Vite設定
└── worker-configuration.d.ts # 型定義

実装の詳細

MCPサーバーの基本を設定する

まず、MCPサーバーの基本構造を設定していきましょう。

// src/mcp.ts
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

// スクリーンショット用の型定義
export type ScreenshotOptions = {
  url: string;
  viewport?: {
    width: number;
    height: number;
  };
  options?: {
    format?: 'png' | 'jpeg';
    quality?: number;
    fullPage?: boolean;
    omitBackground?: boolean;
    waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
  };
};

type State = {}
type Props = {}

export class MyMCP extends McpAgent<Cloudflare.Env, State, Props> {
  server = new McpServer({
    name: "Hello MCP",
    version: "0.0.1",
  });

  async init() {
    // ツールの登録は後述
  }
}

スクリーンショット機能を実装する

Cloudflare Browser Rendering API を使用してスクリーンショットを撮影する関数を実装します。この API は Cloudflare が提供するヘッドレスブラウザサービスで、Web ページのレンダリングとキャプチャを行うことができます。

// src/mcp.ts
export async function takeScreenshot(options: ScreenshotOptions, env: Cloudflare.Env): Promise<string> {
  const accountId = env.CLOUDFLARE_ACCOUNT_ID;
  const apiToken = env.CLOUDFLARE_API_TOKEN;
  
  if (!accountId || !apiToken) {
    throw new Error('Cloudflare Account ID and API Token are required');
  }

  const requestBody = {
    url: options.url,
    viewport: options.viewport || { width: 1920, height: 1080 },
    options: {
      format: options.options?.format || 'png',
      quality: options.options?.quality || 90,
      fullPage: options.options?.fullPage || false,
      omitBackground: options.options?.omitBackground || false,
      waitUntil: options.options?.waitUntil || 'load'
    }
  };

  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${accountId}/browser-rendering/screenshot`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(requestBody)
    }
  );

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Screenshot failed: ${response.status} - ${errorText}`);
  }

  // 画像データをBase64エンコード
  const imageBuffer = await response.arrayBuffer();
  const bytes = new Uint8Array(imageBuffer);
  let binaryString = '';
  const chunkSize = 8192; // 8KB chunks
  
  for (let i = 0; i < bytes.length; i += chunkSize) {
    const chunk = bytes.slice(i, i + chunkSize);
    binaryString += String.fromCharCode(...chunk);
  }
  
  return btoa(binaryString);
}

MCPツールとして機能を登録する

スクリーンショット機能を MCP ツールとして登録することで、AI アシスタントから利用できるようになります。ここでは、パラメータの検証に zod を使用して、型安全性を確保しています。

// src/mcp.ts
export class MyMCP extends McpAgent<Cloudflare.Env, State, Props> {
  // ... 前述のコード

  async init() {
    // スクリーンショット機能の追加
    this.server.tool(
      "take_screenshot",
      "指定したURLのスクリーンショットを取得します。",
      {
        url: z.string().url().describe("スクリーンショットを取得するURL"),
        viewport_width: z.number().optional().default(1920).describe("ビューポートの幅(デフォルト: 1920)"),
        viewport_height: z.number().optional().default(1080).describe("ビューポートの高さ(デフォルト: 1080)"),
        format: z.enum(['png', 'jpeg']).optional().default('png').describe("画像フォーマット(デフォルト: png)"),
        quality: z.number().min(1).max(100).optional().default(90).describe("画像品質(1-100、デフォルト: 90)"),
        full_page: z.boolean().optional().default(false).describe("ページ全体をキャプチャするか(デフォルト: false)"),
        omit_background: z.boolean().optional().default(false).describe("背景を透明にするか(デフォルト: false)"),
        wait_until: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']).optional().default('load').describe("待機条件(デフォルト: load)")
      },
      async (params) => {
        try {

          const screenshotOptions: ScreenshotOptions = {
            url: params.url,
            viewport: {
              width: params.viewport_width,
              height: params.viewport_height
            },
            options: {
              format: params.format,
              quality: params.quality,
              fullPage: params.full_page,
              omitBackground: params.omit_background,
              waitUntil: params.wait_until
            }
          };

          const base64Image = await takeScreenshot(screenshotOptions, this.env);


          return {
            content: [{
              type: "image",
              data: base64Image,
              mimeType: `image/${params.format}`
            }]
          };
        } catch (error) {
          throw error;
        }
      }
    );

  }
}

Hono アプリケーションと統合する

MCPサーバーを Hono アプリケーションに統合することで、HTTP エンドポイントとして公開できるようになります。

// src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer'
import { MyMCP, takeScreenshot } from './mcp';

const app = new Hono()

app.use(renderer)

// MCPサーバーのマウント
app.mount('/sse', MyMCP.serve('/sse').fetch, { replaceRequest: false})
app.mount('/mcp', MyMCP.serve('/mcp').fetch, { replaceRequest: false})

export default app;
export { MyMCP };

環境設定ファイルを準備する

Cloudflare Workers 用の設定ファイルを作成します。ここでは、Durable Objects と Analytics Engine の設定も含めています。

// wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "mcp-hono",
  "compatibility_date": "2025-03-10",
  "main": "./src/index.tsx",
  "compatibility_flags": ["nodejs_compat", "nodejs_compat_populate_process_env"],
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["MyMCP"]
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "class_name": "MyMCP",
        "name": "MCP_OBJECT"
      }
    ]
  }
}
// worker-configuration.d.ts
declare namespace Cloudflare {
  interface Env {
    CLOUDFLARE_ACCOUNT_ID: string;
    CLOUDFLARE_API_TOKEN: string;
    MCP_OBJECT: DurableObjectNamespace<import("./src/index").MyMCP>;
  }
}

Cloudflare API の設定手順

Account ID と API Token を取得する

Browser Rendering API を使用するためには、Cloudflare の認証情報が必要になります。以下の手順で取得していきましょう。

Account ID の取得方法は簡単で、Cloudflare Dashboard にログインし、右サイドバーにある「Account ID」をコピーするだけです。

API Token の作成は少し手順が多くなります。まずダッシュボードで右上のプロフィールアイコンをクリックし、「My Profile」から「API Tokens」を選択します。次に「Create Token」をクリックして「Custom token」を選択し、以下の権限を設定してください。

Permissions:
- Account: Cloudflare Browser Rendering:Edit

Account Resources:
- Include: {あなたのアカウント}

Browser Rendering API を有効化する

Cloudflare ダッシュボードで「Browser Rendering」を選択し、Browser Rendering API を有効化します。無料枠として月1,000リクエストが利用でき、それを超える場合は $5/1,000リクエストの従量課金となります。

環境変数を設定する

取得した認証情報を Cloudflare Workers の環境変数として設定します。

# Cloudflareの認証情報を設定
wrangler secret put CLOUDFLARE_ACCOUNT_ID
wrangler secret put CLOUDFLARE_API_TOKEN

デプロイと使い方

依存関係をインストールする

必要なパッケージをインストールしていきます。

npm install @modelcontextprotocol/sdk
npm install hono
npm install zod
npm install agents/mcp  # MCPエージェント

ビルドしてデプロイする

ビルドとデプロイのコマンドは以下のとおりです。

# ビルド
npm run build

# デプロイ
wrangler deploy

実装におけるポイント

Base64 エンコーディングの最適化について

大きな画像でもメモリ効率よく処理するため、チャンク単位でエンコーディングを行う工夫をしています。

const chunkSize = 8192; // 8KB chunks
for (let i = 0; i < bytes.length; i += chunkSize) {
  const chunk = bytes.slice(i, i + chunkSize);
  binaryString += String.fromCharCode(...chunk);
}

この方法により、一度に大量のメモリを消費することなく、安定したパフォーマンスを維持できます。

まとめ

本記事では、MCPサーバーを Cloudflare Workers にデプロイし、Browser Rendering API を使用してスクリーンショット機能を実装する方法を解説してきました。

この実装によって得られるメリットは大きく、まず Cloudflare Workers の自動スケーリング機能により、トラフィックの増減に柔軟に対応できるようになります。

今後は、OCR 機能や PDF 生成、複数ページの一括処理など、さらなる機能拡張も検討できるでしょう。この実装をベースに、より高度な Web 自動化ツールへと発展させることも可能です。

参考資料

デジタルキューブ

Discussion