Open66

MCP で Claude Desktop から Cloudflare Workers に Script をデプロイしコードを Github で管理する

kutakutatkutakutat

MCP とは

The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you’re building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.

Model Context Protocol (MCP)は、LLMアプリケーションと外部データソース・ツールをシームレスに統合することを可能にするオープンプロトコルです。AI搭載のIDEの構築、チャットインターフェースの機能強化、カスタムAIワークフローの作成など、どのような用途であっても、MCPはLLMが必要とするコンテキストを接続するための標準的な方法を提供します。

MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープン プロトコルです。MCP は、AI アプリケーション用の USB-C ポートのようなものです。USB-C がデバイスをさまざまな周辺機器やアクセサリに接続するための標準化された方法を提供するのと同様に、MCP は AI モデルをさまざまなデータ ソースやツールに接続するための標準化された方法を提供します。

https://modelcontextprotocol.io/introduction

kutakutatkutakutat

MCP は基本的にはプロトコル。特定のものや実装を指すものではない。

kutakutatkutakutat

MCP の一般的なアーキテクチャ

登場する概念

  • MCP Host: Claude Desktop、IDE、または MCP を介してリソースにアクセスする AI ツールなどのプログラム
  • MCP Client: Server との 1:1 接続を維持するプロトコル クライアント
  • MCP Server: 標準化されたモデル コンテキスト プロトコルを通じて特定の機能を公開する軽量プログラム
  • Local Resource: MCP Server が安全にアクセスできるコンピュータのリソース (データベース、ファイル、サービス)
  • Remote Resource: MCP Server が接続できるインターネット経由 (API 経由など) で利用可能なリソース

https://modelcontextprotocol.io/quickstart#general-architecture

Client は、Host の中で動作し、Server とのコネクションを 1:1 で維持する

kutakutatkutakutat

MCP Server が Your Computer の中に書かれているが、仕様としては MCP Server は リモートにあっても問題ない。

ただし、代表的な Client である Claude Desktop はMCP サポートは現在開発者プレビュー段階であり、マシン上で実行されているローカル MCP Server への接続のみをサポートしている。
Claude Desktop のリモート MCP 接続はまだサポートされていない

また、この統合は Claude Desktop アプリでのみ利用可能であり、Claude Web インターフェイス (claude.ai) では利用できない。

kutakutatkutakutat

MCP Client の一覧はこんな感じ。
分かりづらい表現だが、ここでは Claude Desktop はホストでもあるが、クライアントでもある。

Claude Desktop の他には、Editor やコーディングアシスタントがクライアントとして紹介されている。

https://modelcontextprotocol.io/clients

kutakutatkutakutat

Host, Client, Server の役割

ざっくりとだが
Host が取りまとめ役で、複数の Client <-> Server の 1:1 のコネクションを取りまとめている感じ。
Host が Client を作成し、Client は Server といい感じの双方向のコネクションを持つ。
Server は標準化されたプロトコル(MCP のこと)に沿って、いろんな機能を提供してくれる。

https://spec.modelcontextprotocol.io/specification/architecture/

kutakutatkutakutat

少し細かく仕様を眺めてみる

Host の役割

  • 複数のクライアントインスタンスを作成および管理します
  • クライアント接続の権限とライフサイクルを制御する
  • セキュリティポリシーと同意要件を強制する
  • ユーザーの承認決定を処理する
  • AI/LLMの統合とサンプリングを調整
  • クライアント間のコンテキスト集約を管理します
kutakutatkutakutat

Client の役割

各クライアントはホストによって作成され、分離されたサーバー接続を維持します。
Host は複数の Client を作成および管理し、各 Client は特定の Server と 1:1 の関係を持ちます。

  • サーバーごとに1つのステートフルセッションを確立します
  • プロトコルネゴシエーションと機能交換を処理する
  • プロトコルメッセージを双方向にルーティングする
  • サブスクリプションと通知を管理する
  • サーバー間のセキュリティ境界を維持
kutakutatkutakutat

Server の役割

サーバーは特殊なコンテキストと機能を提供します:

  • MCPプリミティブを介してリソース、ツール、プロンプトを公開する
  • 責任を明確にして独立して運営する
  • クライアントインターフェースを通じてサンプリングを要求する
  • セキュリティ上の制約を尊重する必要がある
  • ローカルプロセスまたはリモートサービスどちらにもなれる
kutakutatkutakutat

サンプリング は特殊そうなので一旦脇においてみる。

そうするとピンときづらいのは

Host

  • ユーザーの承認決定を処理する

Server

  • MCPプリミティブを介してリソース、ツール、プロンプトを公開する
kutakutatkutakutat

MCPプリミティブを介してリソース、ツール、プロンプトを公開する

についてはサーバーが、クライアントに与える構成要素を標準化したものとみえる。

以下の 3 つが主だったもの。

  • プロンプト: 言語モデルのインタラクションをガイドする定義済みのテンプレートまたは指示
  • リソース: モデルに追加のコンテキストを提供する構造化データまたはコンテンツ
  • ツール: モデルがアクションを実行したり情報を取得したりできるようにする実行可能な関数

いろんな機能を Server のプログラムとして実装できるが、Client に公開するにはこの MCP に従ってねという感じ。

kutakutatkutakutat

プロンプト、リソース、ツールは制御する主体が異なる

kutakutatkutakutat

ユーザーの承認決定を処理する

この理解のためにも、Client と Server でどのようにやり取りされるかのライフサイクルを見てみる。

3 つのステップに分かれている

  1. 初期化: Capability Negotiation とプロトコルバージョンの合意
  2. 動作:通常のプロトコル通信
  3. シャットダウン: 接続の正常な終了

kutakutatkutakutat

初期化の Capability Negotiation がこのやり取りを特徴づけてる。
より細かい Capability Negotiation のシーケンスをみてみる。

kutakutatkutakutat

Client 側から Server へツール、リソース、プロンプトの Capacity を使いたいと要求したり、
Server 側から Client の roots(あるパス以下のファイルシステムへのアクセス)、 sampling(言語モデル
の利用) を要求したりして、OK であればその Capacity を使えるようにする感じ。

主な機能の一覧

kutakutatkutakutat

通信の仕組みは 2 つある。

  1. stdio、標準入力と標準出力を介した通信
  2. サーバー送信イベント(SSE)を使用した HTTP

https://spec.modelcontextprotocol.io/specification/basic/transports/

MCP で使われるメッセージプロトコルは大きく 3 種類ある。
それぞれ JSON で形式が決まっている。

  1. Request: リクエストはクライアントからサーバーに送信されるか、またはその逆に送信される。応答が期待される。
  2. Response: リクエストへの返答。
  3. Notification: 通知はクライアントからサーバーに、またはその逆に送信されます。応答は期待されない。

https://spec.modelcontextprotocol.io/specification/basic/messages/

このあたりはまた後で見てみようと思う

kutakutatkutakutat

前後するが、Tool に関してはモデルによって制御されるようになっている。
これはいわゆる Agent を実装するときのイメージに近いので納得。

kutakutatkutakutat

また Tool についてはセキュリティに対する推奨事項として次の事が書いてある。

Host のアプリケーションで次を実現すべきだといっている。MUST ではなくSHOULD。

  • AIモデルに公開されているツールを明確に示すUIを提供する
  • ツールが呼び出されたときに明確な視覚的なインジケーターを挿入する
  • 人間がループ内にいることを確認するために、操作時にユーザーに確認プロンプトを表示します。

Claude Desktop で Tool を使用すると確認などが求められるデザインになっているのは、これを守っている感じ。

冒頭の Host の "ユーザーの承認決定を処理する" は
この Tool や前の Sampling についての承認だと思われる。

ただし現時点では、Sampling については Claude Desktop は未対応。

kutakutatkutakutat

クイックスタートを試してみる

クイックスタートはローカルの PC 上で SQLite の DB に対して Claude Desktop から操作を行うもの。

構成は次のよう

  • Host: Claude Desktop
  • Client: Claude Desktop
  • Server: SQLite MCP Server

https://modelcontextprotocol.io/quickstart

kutakutatkutakutat

前提条件 を参考に。
Python, uv, sqlite が使えるようにしておく。

特定の PATH で sqlite の DB を作成しておく。

# Create a new SQLite database
sqlite3 ~/${YOUR_PATH}/test.db <<EOF
CREATE TABLE products (
  id INTEGER PRIMARY KEY,
  name TEXT,
  price REAL
);

INSERT INTO products (name, price) VALUES
  ('Widget', 19.99),
  ('Gadget', 29.99),
  ('Gizmo', 39.99),
  ('Smart Watch', 199.99),
  ('Wireless Earbuds', 89.99),
  ('Portable Charger', 24.99),
  ('Bluetooth Speaker', 79.99),
  ('Phone Stand', 15.99),
  ('Laptop Sleeve', 34.99),
  ('Mini Drone', 299.99),
  ('LED Desk Lamp', 45.99),
  ('Keyboard', 129.99),
  ('Mouse Pad', 12.99),
  ('USB Hub', 49.99),
  ('Webcam', 69.99),
  ('Screen Protector', 9.99),
  ('Travel Adapter', 27.99),
  ('Gaming Headset', 159.99),
  ('Fitness Tracker', 119.99),
  ('Portable SSD', 179.99);
EOF

kutakutatkutakutat

Claude Desktop に MCP Server の存在を知らせる。
知らせるために設定用の json ファイルを作成する。

次のファイルを作成する。
~/Library/Application Support/Claude/claude_desktop_config.json

設定を書き込む。

claude_desktop_config.json
{
  "mcpServers": {
    "sqlite": {
      "command": "uvx",
      "args": ["mcp-server-sqlite", "--db-path", "/Users/${YOUR_USERNAME}/${YOUR_PATH}/test.db"]
    }
  }
}

kutakutatkutakutat

ファイルを保存して Claude Desktop を再起動すると、この SQLite DB が MCP Server 経由で、Claude Desktop から操作できるようになる。

kutakutatkutakutat

実際に動作を試してみる。

1. テーブルを作成してみる

動作を開始すると利用するツールに対して、使ってもいいか聞かれる。

kutakutatkutakutat

2. テーブルにサンプルデータを追加する

データ作成し、追加し、作成できたか確認してくれる。

kutakutatkutakutat

mcp-server-sqlite をみてみる

MCP Client に教えている SQLite に関する情報はこちら

{
  "mcpServers": {
    "sqlite": {
      "command": "uvx",
      "args": [
        "mcp-server-sqlite",
        "--db-path",
        "/Users/YOUR_USERNAME/${YOUR_PATH}/test.db"
        ]
    }
  }
}

mcp-server-sqlite に引数を渡して実行してる模様。

kutakutatkutakutat
pyproject.toml
[project]
name = "mcp-server-sqlite"
version = "0.6.2"
description = "A simple SQLite MCP server"
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["mcp>=1.0.0"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = ["pyright>=1.1.389"]

[project.scripts]
mcp-server-sqlite = "mcp_server_sqlite:main"

ファイル構造はシンプル。

ls src/mcp_server_sqlite
__init__.py
server.py
kutakutatkutakutat

引数処理して、server.py から server.main() 実行してるだけ。

_init.py
from . import server
import asyncio
import argparse


def main():
    """Main entry point for the package."""
    parser = argparse.ArgumentParser(description='SQLite MCP Server')
    parser.add_argument('--db-path', 
                       default="./sqlite_mcp_server.db",
                       help='Path to SQLite database file')
    
    args = parser.parse_args()
    asyncio.run(server.main(args.db_path))


# Optionally expose other important items at package level
__all__ = ["main", "server"]
kutakutatkutakutat

server.py には SqliteDatabase という SQLite を扱うための Class と main() 関数がある。
SqliteDatabase はさっと流し読み。

class SqliteDatabase:
    def __init__(self, db_path: str):
        self.db_path = str(Path(db_path).expanduser())
        ...

    def _init_database(self):
        """Initialize connection to the SQLite database"""
        ...

    def _synthesize_memo(self) -> str:
        """Synthesizes business insights into a formatted memo"""
        logger.debug(f"Synthesizing memo with {len(self.insights)} insights")
        ...

    def _execute_query(self, query: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
        """Execute a SQL query and return results as a list of dictionaries"""
        logger.debug(f"Executing query: {query}")
        ...
kutakutatkutakutat

Tool. Prompt, Resource それぞれの記載があるが、Tool に絞ってみていく。

Client, Server そして LLM の間でのメッセージフローは以下の通り。
Client が Server に Tool ないか確認しにいって、あとは LLM が Client 経由で Server の関数を呼び出すか決めて使っていく。

Tool に関するわかりやすいところを抜粋する。

server.py
...
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio
...

PROMPT_TEMPLATE = """
The assistants goal is to walkthrough an informative demo of MCP. To demonstrate the Model Context Protocol (MCP) we will leverage this example server to interact with an SQLite database.
....
"""


async def main(db_path: str):
    logger.info(f"Starting SQLite MCP Server with DB path: {db_path}")

    db = SqliteDatabase(db_path)
    server = Server("sqlite-manager")

    ...

    @server.list_tools()
    async def handle_list_tools() -> list[types.Tool]:
        """List available tools"""
        return [
            types.Tool(
                name="read-query",
                description="Execute a SELECT query on the SQLite database",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "SELECT SQL query to execute"},
                    },
                    "required": ["query"],
                },
            ),
            types.Tool(
                name="write-query",
                description="Execute an INSERT, UPDATE, or DELETE query on the SQLite database",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "SQL query to execute"},
                    },
                    "required": ["query"],
                },
            ),
            types.Tool(
                name="create-table",
                description="Create a new table in the SQLite database",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "CREATE TABLE SQL statement"},
                    },
                    "required": ["query"],
                },
            ),
            ...
        ]

    @server.call_tool()
    async def handle_call_tool(
        name: str, arguments: dict[str, Any] | None
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        """Handle tool execution requests"""
        try:
            ...
            if name == "read-query":
                if not arguments["query"].strip().upper().startswith("SELECT"):
                    raise ValueError("Only SELECT queries are allowed for read-query")
                results = db._execute_query(arguments["query"])
                return [types.TextContent(type="text", text=str(results))]

            elif name == "write-query":
                if arguments["query"].strip().upper().startswith("SELECT"):
                    raise ValueError("SELECT queries are not allowed for write-query")
                results = db._execute_query(arguments["query"])
                return [types.TextContent(type="text", text=str(results))]

            elif name == "create-table":
                if not arguments["query"].strip().upper().startswith("CREATE TABLE"):
                    raise ValueError("Only CREATE TABLE statements are allowed")
                db._execute_query(arguments["query"])
                return [types.TextContent(type="text", text="Table created successfully")]

            else:
                raise ValueError(f"Unknown tool: {name}")

        except sqlite3.Error as e:
            return [types.TextContent(type="text", text=f"Database error: {str(e)}")]
        except Exception as e:
            return [types.TextContent(type="text", text=f"Error: {str(e)}")]

    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        logger.info("Server running with stdio transport")
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="sqlite",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )
kutakutatkutakutat

以下のようなデコレーターをハンドラーの関数にそれぞれ付与している。

@server.list_tools() - 利用可能なツールの一覧を提供
@server.call_tool() - ツールの呼び出しを処理

もとの mcp.server から読まれているので
mcp の python-sdk を見に行ってみる
https://github.com/modelcontextprotocol/python-sdk

kutakutatkutakutat

@server.list_tools() については
デコレータを適用した関数を実行し、ServerResult でラップして返却してる

python-sdk/src/mcp/server/__init__.py

class Server:
    ...
    def list_tools(self):
        def decorator(func: Callable[[], Awaitable[list[types.Tool]]]):
            logger.debug("Registering handler for ListToolsRequest")

            async def handler(_: Any):
                tools = await func()
                return types.ServerResult(types.ListToolsResult(tools=tools))

            self.request_handlers[types.ListToolsRequest] = handler
            return func

        return decorator

デコレーターで装飾する関数の戻り値は Tool 配列を期待されている

python-sdk/src/mcp/types.py
class ListToolsResult(PaginatedResult):
    """The server's response to a tools/list request from the client."""

    tools: list[Tool]

class Tool(BaseModel):
    """Definition for a tool the client can call."""

    name: str
    """The name of the tool."""
    description: str | None = None
    """A human-readable description of the tool."""
    inputSchema: dict[str, Any]
    """A JSON Schema object defining the expected parameters for the tool."""
    model_config = ConfigDict(extra="allow")
kutakutatkutakutat

この Tool が @server.list_tools() で装飾された handle_list_tools 関数で使用されていたもの。
MCP で定義される MCPサーバーのうちの Tool を定義する部分。

入力値として次のような Dict を期待することが書かれている、
{ 'query': 'テキストで書かれたSQL 文' }

            types.Tool(
                name="read-query",
                description="Execute a SELECT query on the SQLite database",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string", "description": "SELECT SQL query to execute"},
                    },
                    "required": ["query"],
                },
            ),
kutakutatkutakutat

@server.call_tool() は各種 Params を設定して、デコレートした関数を実行している。

python-sdk/src/mcp/server/__init__.py

class Server:
    ...
    def call_tool(self):
        def decorator(
            func: Callable[
                ...,
                Awaitable[
                    Sequence[
                        types.TextContent | types.ImageContent | types.EmbeddedResource
                    ]
                ],
            ],
        ):
            logger.debug("Registering handler for CallToolRequest")

            async def handler(req: types.CallToolRequest):
                try:
                    results = await func(req.params.name, (req.params.arguments or {}))
                    return types.ServerResult(
                        types.CallToolResult(content=list(results), isError=False)
                    )
                except Exception as e:
                    return types.ServerResult(
                        types.CallToolResult(
                            content=[types.TextContent(type="text", text=str(e))],
                            isError=True,
                        )
                    )

            self.request_handlers[types.CallToolRequest] = handler
            return func

        return decorator
kutakutatkutakutat

呼ばれた Tool の名前ごとに arguments を検証し、期待している query の Dict があるかチェックしている。
そして抽出した query Key に対応する値(SQL 文を期待している) を実行している。

server.py
@server.call_tool()
    async def handle_call_tool(
        name: str, arguments: dict[str, Any] | None
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        """Handle tool execution requests"""
        try:
            ...
            if name == "read-query":
                if not arguments["query"].strip().upper().startswith("SELECT"):
                    raise ValueError("Only SELECT queries are allowed for read-query")
                results = db._execute_query(arguments["query"])
                return [types.TextContent(type="text", text=str(results))]
            ...
kutakutatkutakutat

Tool が変更になったときに Client に知らせる仕様なども定められている。

kutakutatkutakutat

MCP の仕様に則っていれば MCP Server を実装することで、他のサービスと MCP Client が繋げられることがわかった。
これから Claude Desktop <> Cloudflare MCP Server <> Cloudflare API を試していく。

kutakutatkutakutat

Cloudflare MCP Server

こんなことができるようになるMCP Server

新しいWorkerをデプロイして、Durable Objectsのサンプルを追加してください。
D1データベース「...」内のデータについて教えてください。
KVネームスペース「...」のすべてのエントリをR2バケット「...」にコピーしてください。

https://github.com/cloudflare/mcp-server-cloudflare?tab=readme-ov-file

kutakutatkutakutat

Setup の手順は少ない

npx @cloudflare/mcp-server-cloudflare init

の後に Claude Desktop を再起動するだけ。

kutakutatkutakutat

init の中身をみてみる。めぼしいとこだけ

Cloudflare の AuthToken を取得して、MCP Server を設定する claude_desktop_config.json にいい感じに設定を追加してくれてる。

init.ts
export async function init(accountTag: string | undefined) {
  ...
  try {
    getAuthTokens()
  } catch (e: any) {
    ...
  }

  if (isAccessTokenExpired()) {
    updateStatus(`Access token expired, refreshing...`, false)
    if (await refreshToken()) {
      updateStatus('Successfully refreshed access token')
    } else {
      throw new Error('Failed to refresh access token')
    }
  }

  const { result: accounts } = await fetchInternal<FetchResult<AccountInfo[]>>('/accounts')
  ...

  const claudeConfigPath = path.join(
    os.homedir(),
    'Library',
    'Application Support',
    'Claude',
    'claude_desktop_config.json',
  )
  const cloudflareConfig = {
    command: (await which('node')).trim(),
    args: [__filename, 'run', account],
  }

  const configDirExists = isDirectory(path.dirname(claudeConfigPath))
  if (configDirExists) {
    const existingConfig = fs.existsSync(claudeConfigPath)
      ? JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'))
      : { mcpServers: {} }
    if ('cloudflare' in (existingConfig?.mcpServers || {})) {
      updateStatus(
        `${chalk.green('Note:')} Replacing existing Cloudflare MCP config:\n${chalk.gray(JSON.stringify(existingConfig.mcpServers.cloudflare))}`,
      )
    }
    const newConfig = {
      ...existingConfig,
      mcpServers: {
        ...existingConfig.mcpServers,
        cloudflare: cloudflareConfig,
      },
    }
    fs.writeFileSync(claudeConfigPath, JSON.stringify(newConfig, null, 2))
  } else {
    ...
  }
}
kutakutatkutakutat

無事に claude_desktop_config.json が更新されたので Claude Desktop を再起動する

kutakutatkutakutat

申し訳ありません。セキュリティの制限により KV namespace の直接作成ができないようです。

KV namespace を作成する Tool はないので、この操作は実行できない。
こちらで作成して、名前空間を伝えて更新してもらう。

kutakutatkutakutat

ちゃんと名前空間を確認して、ID を取得している。
(スクショには ID 乗せてないが出力に入ってる)

kutakutatkutakutat

先ほどアップしたコードの中身もアップデートして、作成した KV の名前空間を使用するようにしてくれる。

kutakutatkutakutat

作成した API のエンドポイントから ToDo を作成し、無事登録できている。

kutakutatkutakutat

ちなみに、次のようなコードが生成されていた。
ライブラリを使わないようにしたので UUID とかも自作しようとしている。

index.js
// UUID生成のヘルパー関数
function generateUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    const r = Math.random() * 16 | 0;
    const v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

// CORSヘッダーの設定
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
};

// レスポンスヘルパー関数
function jsonResponse(body, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: {
      'Content-Type': 'application/json',
      ...corsHeaders,
    },
  });
}

export default {
  async fetch(request, env) {
    // プリフライトリクエストの処理
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: corsHeaders,
      });
    }

    const url = new URL(request.url);
    const path = url.pathname;

    // ToDo一覧の取得
    if (request.method === 'GET' && path === '/todos') {
      try {
        const todoList = await env.TODO_STORE.get('todos');
        return jsonResponse(todoList ? JSON.parse(todoList) : []);
      } catch (error) {
        return jsonResponse({ error: 'Failed to fetch todos' }, 500);
      }
    }

    // ToDo の追加
    if (request.method === 'POST' && path === '/todos') {
      try {
        const body = await request.json();
        if (!body.title) {
          return jsonResponse({ error: 'Title is required' }, 400);
        }

        const todo = {
          id: generateUUID(),
          title: body.title,
          completed: false,
          createdAt: new Date().toISOString(),
        };

        const existingTodos = await env.TODO_STORE.get('todos');
        const todos = existingTodos ? JSON.parse(existingTodos) : [];
        todos.push(todo);

        await env.TODO_STORE.put('todos', JSON.stringify(todos));
        return jsonResponse(todo, 201);
      } catch (error) {
        return jsonResponse({ error: 'Failed to create todo' }, 500);
      }
    }

    // ToDo の更新
    if (request.method === 'PUT' && path.startsWith('/todos/')) {
      try {
        const id = path.split('/')[2];
        const body = await request.json();

        const existingTodos = await env.TODO_STORE.get('todos');
        if (!existingTodos) {
          return jsonResponse({ error: 'Todo not found' }, 404);
        }

        const todos = JSON.parse(existingTodos);
        const todoIndex = todos.findIndex(todo => todo.id === id);

        if (todoIndex === -1) {
          return jsonResponse({ error: 'Todo not found' }, 404);
        }

        todos[todoIndex] = {
          ...todos[todoIndex],
          ...body,
          updatedAt: new Date().toISOString(),
        };

        await env.TODO_STORE.put('todos', JSON.stringify(todos));
        return jsonResponse(todos[todoIndex]);
      } catch (error) {
        return jsonResponse({ error: 'Failed to update todo' }, 500);
      }
    }

    // ToDo の削除
    if (request.method === 'DELETE' && path.startsWith('/todos/')) {
      try {
        const id = path.split('/')[2];
        const existingTodos = await env.TODO_STORE.get('todos');

        if (!existingTodos) {
          return jsonResponse({ error: 'Todo not found' }, 404);
        }

        const todos = JSON.parse(existingTodos);
        const filteredTodos = todos.filter(todo => todo.id !== id);

        if (filteredTodos.length === todos.length) {
          return jsonResponse({ error: 'Todo not found' }, 404);
        }

        await env.TODO_STORE.put('todos', JSON.stringify(filteredTodos));
        return jsonResponse({ message: 'Todo deleted successfully' });
      } catch (error) {
        return jsonResponse({ error: 'Failed to delete todo' }, 500);
      }
    }

    // 404 Not Found
    return jsonResponse({ error: 'Not found' }, 404);
  }
};
kutakutatkutakutat

Cloudflare MCP Server はいろんな機能が公開されていたが今回使ったのは以下の Tool

Workers Management

  • worker_list: List all Workers in your account
  • worker_get: Get a Worker's script content
  • worker_put: Create or update a Worker script

KV Store Management

  • get_kvs: List all KV namespaces in your account
  • kv_list: List keys in a KV namespace

https://github.com/cloudflare/mcp-server-cloudflare/tree/main

kutakutatkutakutat

Hono による試みと失敗

もともとは Hono でやろうと思っていたが、Contxt Window の問題で実現できなかった。
Cloudflare Workers にファイルをアップする時に、コードの中身がそのままリクエストに展開されるようになっている。
バンドルした際のファイルが Context Window に対しては大きかったので(Hono は十分に軽量で非常にすきなライブラリです。あくまで Context Window に対して)、上限に達してアップできなかった。

kutakutatkutakutat

やろうとしていたことは以下の通り。
最後だけ Context Window の問題でだめだった。
ここで使っている Fetch, Filesysytem はどちらも公式がメンテしている。

気になる方は Githubでまとまっているので参考までに。

  1. ✅️ Fetch MCP Server で Hono のクイックスタートを URL を渡してよみこむ
  2. ✅️ Claude にサンプルコードを作成してもらう
  3. ✅️ Filesysytem MCP Server でローカル PC の特定のディレクトリにコードを置く
  4. ✅️ (Claude Desktop を離れ) npx wrangler deploy --dry-run --outdir dist でバンドル (Claude Desktop に戻る)
  5. ✅️ Filesysytem MCP Server でバンドルした成果物のパスを指定して読み込んでもらう
  6. ❌️ Cloudflare MCP Server で Cloudflare Workers にコードをあげる
kutakutatkutakutat

正直、Hono は Cloudflare Workers と非常に連携しやすいのでこんな方法を取る必要はない。

wrangler deploy コマンドで事足りるし、Github Actions か、Clpudflare Build でデプロイメントパイプラインまで組んだほうが良い。

ただ、Claude Desktop から離れずにほとんどのことが完結する体験は面白い。

kutakutatkutakutat

自作で MCP Server を作成したら、他にも繋げられるので面白そう。

kutakutatkutakutat

Github へのコードのプッシュ

ついでなので作成したコードを Github にプッシュしておく。

https://github.com/modelcontextprotocol/servers/tree/main/src/github

kutakutatkutakutat

おなじみの claude_desktop_config.json に設定する。
Personal Access Token を使う。
Fine Grained でも適切に権限が付与されていれば大丈夫そう。

claude_desktop_config.json
{
  "mcpServers": {
    ...
    "github": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-github"
      ],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
      }
    }
  }
}
kutakutatkutakutat

かなり広範囲なことができる。

  • 自動ブランチ作成: ファイルを作成/更新したり変更をプッシュしたりするときに、ブランチが存在しない場合は自動的に作成されます。
  • 包括的なエラー処理: 一般的な問題に対する明確なエラーメッセージ
  • Git 履歴の保存: 強制プッシュなしで適切な Git 履歴を維持する操作
  • バッチ操作: 単一ファイルと複数ファイルの操作の両方をサポート
  • 高度な検索: コード、問題/PR、ユーザーの検索をサポート

Tool としても 16 種類くらいある。多い。

kutakutatkutakutat

Tool を使いながら Repsoitory の作成とコードのプッシュを実施してくれた。