2025年06月04日
昨日6/3(火)に開催された「Cloudflare Workers Workshop in Tokyo #1」に参加をしました!
タイトルにあるCloudflare Workersを利用してMCPサーバーを立ててみよう、というワークショップです。自分にとって学びの多いワークショップでしたので、記録をかねて日記に残します。
まず参加の理由です。さいきんMCPという単語について目や耳にする機会が増えたのですが、正直さっぱり何が起きているのかわからない。
有名なこの絵をみてもいまいちぴんとこないのが現状でした。クライアントやホスト、リモートサーバーなどいろいろあって理解が難しい。解説ブログなどをみても自分の頭の中にすっと落ちてこない。
そんなふうにもやもやしている中、yusukebeさんよりMCPワークショップについての言及がありました。これから主流となるであろうMCPについて概要を理解しておく必要は感じていて、参加をすることにしました。経験上、頭で理解するよりはまず手を動かす方が早い。
続いてワークショップ内容です。
ざっくりとスケジュール。30分程度でイントロダクション、そのあと1時間でMCP x Cloudflare Workersでなにか作ってみて発表をする、という内容でした。
詳細についてはyusukebeさんがgithubにまとめています。
https://github.com/yusukebe/cloudflare-workers-workshop-01
開発者が作業をしやすいように丁寧にドキュメントをまとめていただきありがたいです。自分はMCPがそもそもわからない状態のスタートでしたが、事前にgithubで予習ができました。
これがなかったらおそらく当日は何もできなかったと思います。
事前準備から当日を含めて作ったものです。
このブログはmicroCMSでコンテンツ配信をしているのですが、そちらに接続するMCPを作成してみました。
import { Hono } from "hono";
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { getMicroCMSClient, getPosts, getPostDetail } from "./libs/microcms";
import { MicroCMSQueries } from "microcms-js-sdk";
export interface Env {
SERVICE_DOMAIN: string;
API_KEY: string;
}
export class QlitreDialyMCP extends McpAgent<Env> {
server = new McpServer({
name: "microCMS blog reader",
version: "0.0.1",
});
async init() {
const serviceDomain = this.env.SERVICE_DOMAIN;
const apiKey = this.env.API_KEY;
const client = getMicroCMSClient(serviceDomain, apiKey);
this.server.tool(
"getPosts",
"Fetch blog posts from microCMS",
{
page: z.number().min(1),
},
async ({ page }) => {
const limit = 20;
const offset = limit * (page - 1);
const queries: MicroCMSQueries = {
limit: limit,
offset: offset,
fields: "id,title,description,publishedAt,updatedAt",
};
const result = await getPosts(client, queries);
const contents = result.contents.map((post) => ({
type: "text" as const,
text: JSON.stringify(post),
}));
return {
content: contents,
};
}
);
this.server.tool(
"searchPosts",
"Search blog posts from microCMS",
{
keyword: z.string().min(1),
},
async ({ keyword }) => {
const limit = 30;
const queries: MicroCMSQueries = {
limit: limit,
fields: "id,title,description,publishedAt,updatedAt",
q: keyword,
};
const result = await getPosts(client, queries);
const contents = result.contents.map((post) => ({
type: "text" as const,
text: JSON.stringify(post),
}));
return {
content: contents,
};
}
);
this.server.tool(
"getDetail",
"Fetch Blog Detail",
{
id: z.string().min(1),
},
async ({ id }) => {
const post = await getPostDetail(client, id);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(post),
},
],
};
}
);
}
}
const app = new Hono();
app.mount("/mcp", QlitreDialyMCP.serve("/mcp").fetch, {
replaceRequest: false,
});
export default app;
APIを作るようにMCP記述のルールに沿ってデータの取得などに関する関数を追加するようなイメージでしょうか。今回は一覧取得と詳細取得と検索に関する記述をしてみました。
これで何ができるのかといいますと、通常のプログラムは1:1でやりたいことを呼び出すイメージです。その連続を記述することで、一つの機能ができていきます。
一方でMCPサーバーは与えられたツールを組み合わせて、「よしなに」処理をしてくれると感じました。
例えばこのMCPサーバーを立てて「ラーメン二郎について書かれた記事を教えて」と聞いてみました。
すると、以下のようにまずは検索を行って記事を絞り込みます。そして記事が見つかったのちに自動的に詳細を取得して言及をしてくれました。
道具を与えるとそれを使って仕事をしてくれるイメージなのでしょうか。思い返すとわれわれも普段は会社でこういうことをしています。計算はExcelで行って、文書はワードで書いて、プレゼンはパワーポイントです。こういう仕事をするツールを与えることで「よしなに」仕事をしてくれるツールなのかと感じました。今回は2つの関数の呼び出しとなりましたが、コンボ攻撃みたいで面白い・・・。凝るともっと連携プレーでいろいろできそうな気もします。
続いて体重MCPです。
自分は毎朝体重をハッシュタグをつけてXに投稿しています。
60kg(+0.1) #kuritterweight
— qlitre (@kuri_tter) June 3, 2025
これに関してちょっとした自動化を行なっています。
LINE Botに体重を送信した際にWebhookをWorkers関数に流しています。
そして投稿用の文字列をLINEに返すと同時にD1にデータを保存、ということをやっています。
ダイエットを始めた2024年の1月からこれをやっています。つまり蓄積した体重データがD1にあります。こいつをMCPで呼び出す処理を書いてみました。
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent<Env> {
server = new McpServer({
name: "kuritter weight management application",
version: "0.0.1",
});
async init() {
this.server.tool(
"getRecentWeight",
"Get kuri_tter recent weight",
{},
async () => {
const result = await this.env.DB.prepare(
"SELECT * FROM DailyWeights ORDER BY date DESC Limit 7"
).all();
return {
content: [
{
type: "text",
text: JSON.stringify(result),
},
],
};
}
);
this.server.tool(
"getMonthlyAverageWeight",
"Get average weight for each month",
{
months: z.number().int().positive().max(60).optional(), // 取得月数 (任意)
},
async ({ months }) => {
const sql = `
SELECT
strftime('%Y-%m', date) AS month,
ROUND(AVG(weight), 1) AS avg_weight
FROM DailyWeights
GROUP BY month
ORDER BY month DESC
${months ? "LIMIT ?" : ""}
`;
const { results } = await this.env.DB.prepare(sql)
.bind(...(months ? [months] : []))
.all();
return {
content: [
{
type: "text",
text: JSON.stringify(results),
},
],
};
}
);
}
}
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
if (url.pathname === "/mcp") {
return MyMCP.serve("/mcp").fetch(request, env, ctx);
}
return new Response("Not found", { status: 404 });
},
};
シンプルに直近7日間の体重を返す関数と、月数を指定して平均体重を返す関数を作ってみました。
すると以下のような指示で平均体重がちゃんと返ってきます。
これだけですげーって驚いていたのですが、講師のyusukebeさんに見せたところ「グラフにできるんじゃない?」というアドバイスをもらいました。
それで試しに、グラフを作るような指示をしてみました。
すると、平均体重を取得したのちにUIページのプログラムを書き始めました。
そして、なんと最終的にはかっこいい体重のグラフページができあがりました。
これ、めっちゃやばいと思いました。
ちなみに推薦もあり突発的に発表もさせていただきました。
MCPワークショップでD1に記録している体重をグラフにする発表をしました!毎朝の体重記録がこんな形で役にたつとは… 。#workers_tech pic.twitter.com/3Gh385y8Cu
— qlitre (@kuri_tter) June 3, 2025
こういう場で発表したのは初めてだったのですが、参加されていた方の感触がよくてうれしかったです。
あと自分の体重がでかい画面で投影されているのも超感動しました。
しかし、MCPと連携してUIができるという点にものすごく可能性を感じました。
ぱっと思いつくところですと、BIツールみたいな使い方ができるのではないでしょうか。データをMCPで取得すればグラフィカルな見せ方ができるはずです。去年との比較、予算対比、、云々かんぬん。
それと、コンテンツが最新のモダンなUI付きで返ってくるという点も興味深い。世の中にある今は埋もれてしまった良質なコンテンツの再発掘につながる可能性もあるのでは、というようなことも思いました。
何もわからないところからはじめて最終的に一応は形のあるものができてよかったです。
なんとなくではありますが、MCPの概要とそのインパクトを感じ取れまして、非常に楽しくもあり意義深いイベントでした。改めて感謝いたします。ではでは。