RORK LABJP
EVENT — Apple holds its Surprise and Shine event today, September 9, starting at 10:00 Pacific. That lands in the small hours of September 10 in JapanEXPECT — Expected are the iPhone 18 Pro and Pro Max, a foldable, the 2nm A20 Pro chip, and release dates for iOS 27 and its sibling updatesWAIT — As this is written the event has not happened yet. Rumor-stage writing and post-announcement writing look identical once they are mixed togetherMAX — Since Rork Max generates native Swift, Apple news is not somebody else's problem. Worth repeating that the standard product still writes React NativeSIMULATOR — Rork Max compiles on cloud Macs and lets you check the result in a streaming iOS simulator inside the browser, with no Xcode and no Mac hardwareSEASON — A new OS is when automated build pipelines wobble most. An article selling convenience owes its readers a word about that wobbleEVENT — Apple holds its Surprise and Shine event today, September 9, starting at 10:00 Pacific. That lands in the small hours of September 10 in JapanEXPECT — Expected are the iPhone 18 Pro and Pro Max, a foldable, the 2nm A20 Pro chip, and release dates for iOS 27 and its sibling updatesWAIT — As this is written the event has not happened yet. Rumor-stage writing and post-announcement writing look identical once they are mixed togetherMAX — Since Rork Max generates native Swift, Apple news is not somebody else's problem. Worth repeating that the standard product still writes React NativeSIMULATOR — Rork Max compiles on cloud Macs and lets you check the result in a streaming iOS simulator inside the browser, with no Xcode and no Mac hardwareSEASON — A new OS is when automated build pipelines wobble most. An article selling convenience owes its readers a word about that wobble
Articles/AI Models
AI Models/2026-08-07Advanced

Cutting MCP Tools Didn't Make Anything Lighter — 2,377 Bytes of Definitions vs 110,298 Bytes of Response

I wrote a minimal MCP server for a wallpaper catalog and measured the byte cost of tool definitions against the byte cost of responses. Here is which side actually matters, and the real reason to merge tools.

MCP4Rork558React Native236Expo203Architecture22Measurement2advanced7

Premium Article

The MCP server behind my wallpaper app catalog had quietly grown to twelve tools.

List, search, find by condition, fetch one. The kind of accumulation where each addition seems reasonable on its own.

Working as an indie developer across several apps, tooling like this grows without anyone else ever reviewing it. That is usually why the growth goes unnoticed.

Assuming the count was the problem, I removed the six least-used tools. Nothing felt different.

Either I had not cut deeply enough, or I had cut in the wrong place. Guessing between those two is how afternoons disappear.

So I measured instead.

A dependency-free MCP server, written for measurement

Using the official SDK adds framing and session bookkeeping to every byte count. Since I wanted to isolate what costs what, I implemented JSON-RPC 2.0 over stdio directly.

MCP is, at its core, one JSON-RPC message per line on standard input and output. Supporting only initialize, tools/list, and tools/call takes about this much code.

#!/usr/bin/env node
// server.mjs — a minimal MCP server built for measurement (stdio / JSON-RPC 2.0)
import { createInterface } from "node:readline";
 
// 324 wallpaper records, matching the field layout of the real app
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: "Lists wallpapers in the catalog. Can be narrowed by category.",
    inputSchema: { type: "object", properties: {
      category: { type: "string", description: "Category name" },
      limit: { type: "integer", description: "Maximum number of records" } } } },
  { name: "search_wallpapers",
    description: "Searches catalog wallpapers by keyword across tags and titles.",
    inputSchema: { type: "object", properties: {
      query: { type: "string", description: "Search term" },
      limit: { type: "integer", description: "Maximum number of records" } }, required: ["query"] } },
  // …plus 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;                 // Skip blank lines, or JSON.parse throws here
  const req = JSON.parse(line);
  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; // Variable tool count, for measurement only
    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");
});

Everything below was measured on Node.js v22.22.3 under Linux, with the server running as a child process and requests sent across standard input and output. These are not device numbers or numbers over a network — they are byte counts and local round trips.

The extra n parameter on tools/list is not part of the spec. It exists so I could vary the tool count one at a time and watch the response size move.

Tool definitions cost two orders of magnitude less than I assumed

Here is the size of the tools/list result, counted in UTF-8 bytes.

Toolstools/list responseAverage per tool
1297 bytes297 bytes
3873 bytes291 bytes
51,260 bytes252 bytes
71,665 bytes238 bytes
102,106 bytes211 bytes
122,377 bytes198 bytes

Dividing the span from one tool to twelve gives a marginal cost of 189.1 bytes per additional tool.

The six tools I had deleted therefore added up to roughly 1.2 KB.

That number alone explains why nothing changed. 1.2 KB is not a quantity anyone notices.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
12 tool definitions cost 2,377 bytes; one response cost 110,298 bytes — measured, not estimated
A paginator that slices on record boundaries from a byte budget, with 99.3–100.0% fill rates
Measuring bigram similarity between tool descriptions to find the 45.5% collision worth merging
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

AI Models2026-04-02
Rork × MCP (Model Context Protocol) Complete Implementation Guide — Building Next-Gen Mobile Apps with AI Tool Integration
A complete guide to integrating MCP (Model Context Protocol) into your Rork app. Covers architecture design, streaming, authentication, and production operations with working code examples.
AI Models2026-06-19
Before You Pay $200/mo for Rork Max, Map How Far Expo Reaches in Three Tiers
Wanting widgets or Live Activities makes Rork Max tempting, but most of those features are reachable from the Expo setup that standard Rork generates. Here is how I sort each Apple-native feature into three tiers—reachable in Expo, reachable with a custom module, or where Max is the pragmatic answer—and verify which tier my app is in before paying.
AI Models2026-06-14
Calling Apple Foundation Models from a Rork (Expo) App: Bridging On-Device AI Through a Native Module
Rork generates Expo (React Native) apps, but Apple Foundation Models ships as a Swift framework you can't touch from JavaScript. Here's how to write an Expo Modules API bridge, gate it by availability, and fall back to the cloud on unsupported devices.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links