0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

趣味で作ったMCPサーバーを本番で動かすまでに踏んだ罠まとめ

0
Posted at

はじめに

前回、「日本の公開データを改ざん検知つきでAIに渡すMCP」を作った話を書いたんですけど、その続きです。

コードはほぼAI(Claude)に書いてもらってるんですが、「動くコードができる」と「本番でちゃんと動く」の間には、想像の3倍くらい谷があったんですよね…。ローカルでは全部緑だったのに、Cloudflare Workersに上げた瞬間に色々ぶっ壊れました。

同じように「AIと一緒にMCPサーバー作って本番に上げよう」としてる人がいたら、たぶん同じ穴に落ちると思うので、踏んだ罠を正直に置いておきます。

環境は Cloudflare Workers + TypeScript + D1(SQLite) です。


罠1: Cloudflare Cron、曜日指定が通らない

「このデータは週1回でいいや」と思って、cronをこう書いたんです。

crons = ["30 20 * * 0"]   # 毎週日曜

デプロイしたらこれ。

✘ [ERROR] invalid cron string: 30 20 * * 0 [code: 10100]

30 20 * * 0 は普通のcronとしては正しい(日曜の20:30)はずなのに、Cloudflareに弾かれる。調べると、Cloudflare Cron Triggers は曜日指定(5番目のフィールド)をうまく受け付けないみたいで。

結局、素直に日次にしました。

crons = ["30 20 * * *"]   # 毎日

週1回のはずを毎日叩くのはちょっと無駄に見えるけど、「変化がなければ何も起きない」作りにしてあるので実害なし。曜日cronを使いたくなったら日次で代用、が一番ハマらないです。


罠2: 「1行ずつINSERT」で本番が死ぬ(サブリクエスト上限)

これが一番ハマりました。

数千件のデータを取り込むとき、最初は素直に「1行ずつ INSERT / UPDATE」してたんです。ローカルのテストでは件数が少ないので普通に通る。で、本番で全国分を流したら、これ。

Too many API requests by single Worker invocation.

Cloudflare Workersは、1回の実行で投げられる外部リクエスト(DBアクセス含む)に上限があるんですよね(有料プランで1000)。1行ごとにDBを叩いてると、数千行で簡単に超えます。

直し方は、

  • 既存データを 1回のSELECTでまとめて取得
  • 差分は メモリ上で計算
  • 書き込みは 100件ずつ束ねて(batch) 投げる

これで、数千行でも実行あたりのリクエストが数十回に収まって、上限に余裕で収まりました。「ループの中で毎回DBを叩いてないか?」は、Workersだと最初に疑うポイントです。


罠3: 役所のデータ、gzipで返ってくる

国交省のAPIを叩いたら、レスポンスがgzip圧縮で返ってきました。

ありがたいことに、サーバーが Content-Encoding: gzip をちゃんと付けてくれていれば、Workersの fetch()勝手に解凍してくれるので .json() でそのまま読めます。

ただ、付いてないケースもあるらしいので、保険で DecompressionStream('gzip') のフォールバックも入れておきました。「ローカルのcurl(--compressed)では読めたのに本番で文字化け」みたいなときは、ここを疑うといいです。


罠4: 初回にDiscordへ数万件の通知を爆撃した

このサービス、変化があったらDiscordに通知を飛ばすようにしてるんですけど。

台帳を作った一番最初って、過去ぶんが全部「新規」として入ってくるんですよね。それを全部「新規です!」って通知に流したら、Discordに3万件以上のメッセージが飛んでいきました…。

対策は、「初回の取り込み(まだ1件もデータが無い状態)のときは通知しない」というガードを入れること。差分アラート系を作るときは、初回バックフィルだけは黙らせるのを最初から入れておかないと、自分のWebhookが死にます。


罠5: データのフィールド名が、年版で意味が変わる(schema drift)

地価公示のデータ(GeoJSON)を取り込んだときの話。

このデータ、フィールド名が L01_001, L01_002… みたいなただの番号で来るんです。で、怖いのが、同じ番号でも年版(仕様バージョン)によって指してる中身が違うこと。たとえば「公示価格」が、ある年は L01_006、別の年は L01_008 だったり。

これに気づかず番号でハードコードしてると、ある日こっそり別の列を「価格」として読み込んで、それっぽい嘘データを記録し続けるという最悪の事故が起きます。改ざん検知つきの台帳でこれをやったら本末転倒…。

なので、取り込み時に「値の妥当性チェック(sanity check)」を入れました。

  • 年が4桁で、今年の近くか
  • 価格がint型で、現実的な範囲か
  • 座標が日本の範囲内か

これが1つでも崩れたら、そのデータは取り込まずに中止してアラートを出す。「黙って続行」じゃなくて「おかしかったら止まる」にしておくのが、データの信頼性を売りにするなら必須でした。


おまけ: MCPエンドポイントの形

ちなみに、出来上がったMCPサーバーは Streamable HTTP(JSON-RPC) で、Claude Desktopの設定にURLを足すだけで使えます。

{
  "mcpServers": {
    "japan-public-ledgers": {
      "url": "https://mcp.mcp-revenue-empire.com/mcp"
    }
  }
}

ツール一覧はcurlでも見れます。

curl https://mcp.mcp-revenue-empire.com/mcp/tools

まとめ

  • AIにコードを書いてもらっても、本番(Cloudflare Workers)の制約とデータの癖には自分でぶつかる
  • 特にハマったのは「曜日cron不可」「サブリクエスト上限(1行ずつDBが死ぬ)」「初回通知の爆撃」「フィールドのschema drift」
  • でも、こういう「動かしてみて初めて分かる」部分が、いじってて一番面白いところでもあります

同じ穴に落ちた人の検索に引っかかれば本望です。「ここもっとこうした方がいいよ」があれば、コメントで教えてください〜!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?