◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り26日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り26日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/AIモデル
◉ AIモデル/2026-08-07上級

MCP のツールを減らしても軽くならない — 定義 2,377 バイトと応答 110,298 バイトを測り直した記録

壁紙カタログ用のMCPサーバを依存なしの最小構成で書き直し、ツール定義2,377バイトと応答110,298バイトを実測した記録です。削るべきは応答側であること、ツール統合の本当の理由が見分けやすさにあることを、測定ハーネスと3つの設計ルールとともに示します。

MCP4Rork612React Native243Expo234設計13実測advanced4

✦ プレミアム記事

壁紙アプリのカタログを扱う MCP サーバのツールが、気づけば12本になっていました。

一覧、検索、条件指定、詳細取得。似た用途のものが少しずつ増えていく、あの膨らみ方です。

個人開発でアプリを複数抱えていますと、こうした補助の仕組みは誰にも相談せず自分ひとりで育てることになります。膨らみに気づくのが遅れるのは、たいていそのためです。

「多すぎるから重いのだろう」と考えて、使用頻度の低い6本を外しました。ところが体感はほとんど変わりません。

削った量が足りなかったのか、そもそも削る場所が違ったのか。判断がつかないまま手を動かし続けるのは、いちばん時間を溶かすやり方です。

測ることにしました。

測定用に、依存のない MCP サーバを書き直す

公式 SDK を使うと、フレーミングやセッション管理の分だけバイト数が上乗せされます。何が何バイトなのかを切り分けたかったので、stdio 上の JSON-RPC 2.0 を素で実装しました。

MCP は要するに「1行1メッセージの JSON-RPC を標準入出力で流す」プロトコルです。initialize / tools/list / tools/call の3つだけなら、この程度の分量で動きます。

カタログの 324 件は、App Store と Google Play に出している壁紙アプリの実データとほぼ同じフィールド構成にしてあります。フィールドを削って測っても、手元の判断材料にはなりませんので。

#!/usr/bin/env node
// server.mjs — 測定用の最小 MCP サーバ(stdio / JSON-RPC 2.0)
import { createInterface } from "node:readline";
 
// 壁紙カタログ 324 件(実アプリのフィールド構成に合わせています)
const CATALOG = Array.from({ length: 324 }, (_, i) => ({
  id: `wp_${String(i + 1).padStart(4, "0")}`,
  title: `Wallpaper ${i + 1}`,
  category: ["nature", "abstract", "minimal", "space", "city"][i % 5],
  width: 2796,
  height: 1290,
  bytes: 380000 + (i * 977) % 220000,
  sha256: Array.from({ length: 64 }, (_, k) => "0123456789abcdef"[(i * 7 + k) % 16]).join(""),
  tags: ["calm", "blue", "gradient", "night"].slice(0, (i % 4) + 1),
  license: "internal",
  createdAt: "2026-05-01T00:00:00Z",
  updatedAt: "2026-07-30T00:00:00Z",
  overlayScrim: { top: 0.42, bottom: 0.18 },
}));
 
const TOOLS = [
  { name: "list_wallpapers",
    description: "カタログの壁紙を一覧します。カテゴリで絞り込めます。",
    inputSchema: { type: "object", properties: {
      category: { type: "string", description: "カテゴリ名" },
      limit: { type: "integer", description: "最大件数" } } } },
  { name: "search_wallpapers",
    description: "カタログの壁紙をキーワードで検索します。タグと題名が対象です。",
    inputSchema: { type: "object", properties: {
      query: { type: "string", description: "検索語" },
      limit: { type: "integer", description: "最大件数" } }, required: ["query"] } },
  // …以下、find_wallpapers / get_wallpaper / get_locale_keys /
  //   get_missing_translations / check_i18n_completeness / get_build_config /
  //   get_app_config / list_categories / get_catalog_stats / validate_catalog
];
 
function call(name, args = {}) {
  if (name === "list_wallpapers") {
    let rows = CATALOG;
    if (args.category) rows = rows.filter((r) => r.category === args.category);
    return rows.slice(0, args.limit ?? rows.length);
  }
  if (name === "list_wallpapers_projected") {
    let rows = CATALOG;
    if (args.category) rows = rows.filter((r) => r.category === args.category);
    return rows.slice(0, args.limit ?? rows.length)
      .map((r) => ({ id: r.id, title: r.title, category: r.category }));
  }
  if (name === "get_wallpaper") return CATALOG.find((r) => r.id === args.id) ?? null;
  if (name === "list_categories") return [...new Set(CATALOG.map((r) => r.category))];
  return { ok: true };
}
 
const rl = createInterface({ input: process.stdin });
rl.on("line", (line) => {
  if (!line.trim()) return;                 // 空行は無視。ここを忘れると JSON.parse で落ちます
  const req = JSON.parse(line);
  if (req.id === undefined) return;        // id のない通知(notifications/initialized など)には応答しません
  let result;
  if (req.method === "initialize") {
    result = {
      protocolVersion: "2025-06-18",
      capabilities: { tools: {} },
      serverInfo: { name: "wallpaper-catalog", version: "0.1.0" },
    };
  } else if (req.method === "tools/list") {
    const n = req.params?.n ?? TOOLS.length; // 測定用にツール数を可変にしています
    result = { tools: TOOLS.slice(0, n) };
  } else if (req.method === "tools/call") {
    const payload = call(req.params.name, req.params.arguments);
    result = { content: [{ type: "text", text: JSON.stringify(payload) }] };
  } else {
    result = {};
  }
  process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: req.id, result }) + "\n");
});

計測に使った環境は Node.js v22.22.3 の Linux 上で、子プロセスとして起動した本サーバに対し標準入出力越しに要求を投げています。以下の数値はすべてこの構成で実際に走らせて取得したものです。実機やネットワーク越しの数字ではありません。

実際のクライアントは initialize のあとに notifications/initialized を送ってきます。id を持たない通知には応答を返さないのが JSON-RPC の決まりで、上のサーバは req.id === undefined のとき何も書かずに戻ります。ここを書き忘れて通知にまで応答を返すと、クライアントによっては身に覚えのない id の行として扱われ、原因の見えにくい不整合になりかねません。

tools/list にわざと n パラメータを足してあるのは、ツール数を1本ずつ変えながら応答サイズを測るためです。仕様外の余計な引数ですが、測定用のサーバなので割り切りました。

ツール定義のコストは、想像していたより二桁小さい

まず tools/list の応答サイズです。ツール数を変えながら、返ってきた result を UTF-8 のバイト数で数えました。

ツール数tools/list の応答1本あたり平均
1297 bytes297 bytes
3873 bytes291 bytes
51,260 bytes252 bytes
71,665 bytes238 bytes
102,106 bytes211 bytes
122,377 bytes198 bytes

1本目から12本目までの差を本数で割ると、追加1本あたりの限界コストは 189.1 バイト でした。

つまり私が外した6本は、合計でおよそ 1.2 KB です。

この時点で、削ったのに変わらなかった理由の見当がつきました。1.2 KB は、そもそも体感が変わる大きさではありません。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦ツール定義12本で 2,377 バイト、応答1回で 110,298 バイト。削るべき側を実測で特定します
✦バイト予算からレコード境界で切り出すページャの実装。充填率 99.3〜100.0% を実測しています
✦ツール名と説明の文字bigram類似度を測り、45.5% で衝突していた2本を統合するまでの手順
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-06-22
壊れたキャッシュで毎回起動時に落ちるアプリ——ユーザーが自力で抜け出せる「セーフモード起動」を設計する
永続化したキャッシュが壊れて起動のたびに同じ場所で落ちると、ユーザーには再インストールしか残りません。アプリ自身が早期クラッシュの連続を数え、対話可能になって初めて起動を確定し、危険な状態だけを段階的にリセットするセーフモードを、Expo(React Native)の実装で設計します。
◉ AI モデル2026-10-25
Rork で作ったアプリが Sonnet 4.5 を呼んでいないか、11月30日の前に洗い出します
Claude Sonnet 4.5 の API 提供終了は 2026年11月30日の予定です。アプリ内に残ったモデル ID を数分で洗い出すスクリプトと、見つかった場所ごとの直し方・順番をまとめました。
◉ AI モデル2026-09-12
iOS 27 の新機能を Rork に頼むと、エラーにならないまま古い書き方へ戻ります
9月14日に iOS 27 が配信されます。生成 AI が知らない API を頼まれたときには、落ちる失敗と落ちない失敗の二種類が起きます。標準 Rork と Rork Max で性質が違う理由と、返ってきたコードを確かめる三つの順番を整理しました。