Unified · Split
| @@ -25,7 +25,7 @@ Run it your way: local CLI, a desktop app, hosted Executor Cloud, or self-hosted · 24 unmodified lines | |||
| 25 | 25 | ||
| 26 | ## Blog | 26 | ## Blog |
| 27 | 27 | ||
| 28 | - [MCPs, CLIs, and APIs are the same thing](https://executor.sh/blog/mcp-cli-same-thing): interactive tour of tool-calling envelopes, why context bloat is a client problem, and what the MCP-vs-CLI paper actually measured. | 28 | - [MCPs, CLIs, and APIs are the same thing](https://executor.sh/blog/mcp-cli-same-thing): interactive tour of tool-calling envelopes, why context bloat is a client problem, and why the MCP-vs-CLI debate is a category error. |
| 29 | - [Why MCP had so many growing pains](https://executor.sh/blog/why-mcp-had-growing-pains): what went wrong in MCP's first year and where it goes from here. | 29 | - [Why MCP had so many growing pains](https://executor.sh/blog/why-mcp-had-growing-pains): what went wrong in MCP's first year and where it goes from here. |
| 30 | 30 | ||
| 31 | ## Source | 31 | ## Source |
| @@ -12,10 +12,9 @@ import { fmt, useAnimatedNumber } from "./shared"; · 11 unmodified lines | |||
| 12 | * (on demand or everything up front). Flipping the interface barely moves the | 12 | * (on demand or everything up front). Flipping the interface barely moves the |
| 13 | * number; flipping the loading strategy moves it ~20x. | 13 | * number; flipping the loading strategy moves it ~20x. |
| 14 | * | 14 | * |
| 15 | * Figures are illustrative but anchored: the arXiv paper everyone cited | 15 | * Figures are illustrative: the official GitHub MCP server's 44-tool catalog |
| 16 | * measured the official GitHub MCP server at 44 tools, eager clients resending | 16 | * at roughly 800 tokens per tool schema, resent every request by eager |
| 17 | * the whole catalog every request, and ~22k tokens for 27 built-in tool | 17 | * clients. |
| 18 | * schemas (~800 tokens per schema). | ||
| 19 | */ | 18 | */ |
| 20 | 19 | ||
| 21 | type Interface_ = "cli" | "mcp"; | 20 | type Interface_ = "cli" | "mcp"; |
| @@ -1,158 +0,0 @@ | |||
| 1 | "use client"; | ||
| 2 | |||
| 3 | /* eslint-disable react/forbid-elements -- blog widgets use bespoke styled | ||
| 4 | controls; the product design-system <Button> does not model them. */ | ||
| 5 | |||
| 6 | import { useState } from "react"; | ||
| 7 | import { fmt } from "./shared"; | ||
| 8 | |||
| 9 | /** | ||
| 10 | * What the paper actually measured. Stage one shows Table 2 — median input | ||
| 11 | * tokens per completed run for seven harnesses on the same GitHub task — and | ||
| 12 | * asks the reader which runs had an MCP server attached. The reveal: none of | ||
| 13 | * them; the 28x spread is pure harness overhead. Stage two shows Table 3 — | ||
| 14 | * what happened when the MCP server *was* attached — where four of five | ||
| 15 | * harnesses got cheaper. | ||
| 16 | * | ||
| 17 | * Data: arXiv 2608.08654, Tables 2 and 3 (median input tokens, completed runs). | ||
| 18 | */ | ||
| 19 | |||
| 20 | type Row = { readonly name: string; readonly tokens: number }; | ||
| 21 | |||
| 22 | const TABLE2: ReadonlyArray<Row> = [ | ||
| 23 | { name: "pi", tokens: 14660 }, | ||
| 24 | { name: "Tau", tokens: 16459 }, | ||
| 25 | { name: "Codex", tokens: 82378 }, | ||
| 26 | { name: "Hermes", tokens: 83954 }, | ||
| 27 | { name: "opencode", tokens: 137800 }, | ||
| 28 | { name: "qwen-code", tokens: 297649 }, | ||
| 29 | { name: "Claude Code", tokens: 410797 }, | ||
| 30 | ]; | ||
| 31 | const MAX2 = 410797; | ||
| 32 | |||
| 33 | type Paired = { readonly name: string; readonly ratio: number; readonly note?: string }; | ||
| 34 | |||
| 35 | // Table 3: cost with MCP attached ÷ cost without, per harness. | ||
| 36 | const TABLE3: ReadonlyArray<Paired> = [ | ||
| 37 | { name: "Claude Code", ratio: 0.57 }, | ||
| 38 | { name: "qwen-code", ratio: 0.74 }, | ||
| 39 | { name: "Hermes", ratio: 0.84 }, | ||
| 40 | { name: "opencode", ratio: 0.95 }, | ||
| 41 | { name: "Codex", ratio: 16.09, note: "n=2" }, | ||
| 42 | ]; | ||
| 43 | |||
| 44 | // Position on a log scale from 0.4x to 20x, so 1.0x sits at a fixed line and | ||
| 45 | // the Codex outlier stays on the chart without flattening everything else. | ||
| 46 | const logPos = (r: number) => { | ||
| 47 | const min = Math.log(0.4); | ||
| 48 | const max = Math.log(20); | ||
| 49 | return ((Math.log(r) - min) / (max - min)) * 100; | ||
| 50 | }; | ||
| 51 | |||
| 52 | type Stage = "guess" | "revealed" | "paired"; | ||
| 53 | |||
| 54 | export function HarnessSpreadDemo() { | ||
| 55 | const [stage, setStage] = useState<Stage>("guess"); | ||
| 56 | |||
| 57 | return ( | ||
| 58 | <div className="bw"> | ||
| 59 | <p className="sr-only" aria-live="polite"> | ||
| 60 | {stage === "guess" | ||
| 61 | ? "Seven harnesses ran the same GitHub task. Median input tokens range from 14,660 to 410,797." | ||
| 62 | : stage === "revealed" | ||
| 63 | ? "Reveal: none of these runs had an MCP server attached. The 28x spread is harness overhead." | ||
| 64 | : "With the MCP server attached, four of five harnesses got cheaper. Median paired ratio 0.93."} | ||
| 65 | </p> | ||
| 66 | |||
| 67 | {stage !== "paired" ? ( | ||
| 68 | <> | ||
| 69 | <div className="bw-head"> | ||
| 70 | <span className="bw-eyebrow">Seven harnesses, one task, median input tokens</span> | ||
| 71 | </div> | ||
| 72 | <div className="bw-bars"> | ||
| 73 | {TABLE2.map((r) => ( | ||
| 74 | <div key={r.name} className="bw-bars__row"> | ||
| 75 | <span className="bw-bars__name">{r.name}</span> | ||
| 76 | <div className="bw-bars__track"> | ||
| 77 | <div | ||
| 78 | className="bw-bars__fill" | ||
| 79 | style={{ width: `${Math.max(2, (r.tokens / MAX2) * 100)}%` }} | ||
| 80 | /> | ||
| 81 | </div> | ||
| 82 | <span className="bw-bars__val">{fmt(r.tokens)}</span> | ||
| 83 | </div> | ||
| 84 | ))} | ||
| 85 | </div> | ||
| 86 | {stage === "guess" ? ( | ||
| 87 | <div className="bw-quiz"> | ||
| 88 | <span>Which of these runs had an MCP server attached?</span> | ||
| 89 | <button type="button" className="bw-btn" onClick={() => setStage("revealed")}> | ||
| 90 | Reveal | ||
| 91 | </button> | ||
| 92 | </div> | ||
| 93 | ) : ( | ||
| 94 | <div className="bw-quiz bw-quiz--answer"> | ||
| 95 | <span> | ||
| 96 | <strong>None of them.</strong> No MCP server was attached to any run in this chart — | ||
| 97 | the 28x spread between the cheapest and most expensive harness is pure harness | ||
| 98 | overhead. | ||
| 99 | </span> | ||
| 100 | <button type="button" className="bw-btn" onClick={() => setStage("paired")}> | ||
| 101 | So what happened when they attached it? | ||
| 102 | </button> | ||
| 103 | </div> | ||
| 104 | )} | ||
| 105 | </> | ||
| 106 | ) : ( | ||
| 107 | <> | ||
| 108 | <div className="bw-head"> | ||
| 109 | <span className="bw-eyebrow">Cost with MCP attached ÷ cost without, per harness</span> | ||
| 110 | </div> | ||
| 111 | <div className="bw-ratio" style={{ "--one-x": `${logPos(1)}%` } as React.CSSProperties}> | ||
| 112 | <div className="bw-ratio__row bw-ratio__row--axis" aria-hidden="true"> | ||
| 113 | <span className="bw-ratio__name" /> | ||
| 114 | <div className="bw-ratio__axis"> | ||
| 115 | <span style={{ left: `${logPos(0.5)}%` }}>0.5×</span> | ||
| 116 | <span style={{ left: `${logPos(1)}%` }}>1×</span> | ||
| 117 | <span style={{ left: `${logPos(2)}%` }}>2×</span> | ||
| 118 | <span style={{ left: `${logPos(5)}%` }}>5×</span> | ||
| 119 | <span style={{ left: `${logPos(16)}%` }}>16×</span> | ||
| 120 | </div> | ||
| 121 | </div> | ||
| 122 | {TABLE3.map((p) => ( | ||
| 123 | <div key={p.name} className="bw-ratio__row"> | ||
| 124 | <span className="bw-ratio__name"> | ||
| 125 | {p.name}{" "} | ||
| 126 | <span className="bw-ratio__val"> | ||
| 127 | {p.ratio}×{p.note ? ` (${p.note})` : ""} | ||
| 128 | </span> | ||
| 129 | </span> | ||
| 130 | <div className="bw-ratio__track"> | ||
| 131 | <span | ||
| 132 | className="bw-ratio__marker" | ||
| 133 | data-cheaper={p.ratio < 1 ? "true" : undefined} | ||
| 134 | style={{ left: `${logPos(p.ratio)}%` }} | ||
| 135 | /> | ||
| 136 | </div> | ||
| 137 | </div> | ||
| 138 | ))} | ||
| 139 | <div className="bw-ratio__legend"> | ||
| 140 | <span>← cheaper with MCP</span> | ||
| 141 | <span>more expensive with MCP →</span> | ||
| 142 | </div> | ||
| 143 | </div> | ||
| 144 | <div className="bw-quiz bw-quiz--answer"> | ||
| 145 | <span> | ||
| 146 | Four of five harnesses got <strong>cheaper</strong> with the MCP server attached. | ||
| 147 | Across the thirteen strictly paired runs the median ratio is <strong>0.93</strong> — | ||
| 148 | the authors call the comparison inconclusive. | ||
| 149 | </span> | ||
| 150 | <button type="button" className="bw-btn" onClick={() => setStage("guess")}> | ||
| 151 | Back to the spread | ||
| 152 | </button> | ||
| 153 | </div> | ||
| 154 | </> | ||
| 155 | )} | ||
| 156 | </div> | ||
| 157 | ); | ||
| 158 | } | ||
| @@ -1,6 +1,6 @@ | |||
| 1 | --- | 1 | --- |
| 2 | title: "MCPs, CLIs, and APIs are the same thing" | 2 | title: "MCPs, CLIs, and APIs are the same thing" |
| 3 | description: "You can compile an MCP server into a CLI without changing a single tool. An interactive look at what tool calling actually is, why context bloat is a client problem, and what that viral paper really measured." | 3 | description: "You can compile an MCP server into a CLI without changing a single tool. An interactive look at what tool calling actually is, why context bloat is a client problem, and why the MCP-vs-CLI debate is a category error." |
| 4 | date: 2026-08-23 | 4 | date: 2026-08-23 |
| 5 | author: "Rhys Sullivan" | 5 | author: "Rhys Sullivan" |
| 6 | --- | 6 | --- |
| @@ -8,10 +8,9 @@ author: "Rhys Sullivan" · 1 unmodified line | |||
| 8 | import { SameCallDemo } from "../../components/blog/same-call-demo"; | 8 | import { SameCallDemo } from "../../components/blog/same-call-demo"; |
| 9 | import { McpToCliDemo } from "../../components/blog/mcp-to-cli-demo"; | 9 | import { McpToCliDemo } from "../../components/blog/mcp-to-cli-demo"; |
| 10 | import { ContextFloodDemo } from "../../components/blog/context-flood-demo"; | 10 | import { ContextFloodDemo } from "../../components/blog/context-flood-demo"; |
| 11 | import { HarnessSpreadDemo } from "../../components/blog/harness-spread-demo"; | ||
| 12 | import { InstallRaceDemo } from "../../components/blog/install-race-demo"; | 11 | import { InstallRaceDemo } from "../../components/blog/install-race-demo"; |
| 13 | 12 | ||
| 14 | Last week a [paper](https://arxiv.org/abs/2608.08654) made the rounds claiming that agents finish software tasks just as reliably without MCP, and 5 to 28 times cheaper. The replies sorted themselves into the usual camps: MCP is dead, CLIs won, protocols were a mistake. | 13 | Every few weeks the debate comes back around: CLIs beat MCP, MCP is dead, agents just need bash. Benchmarks get run, threads get written, and everyone sorts into camps. |
| 15 | 14 | ||
| 16 | The whole argument is built on a category error. MCP servers, CLIs, and APIs are not three competing technologies. They are three envelopes around the same thing: an agent invoking a tool. | 15 | The whole argument is built on a category error. MCP servers, CLIs, and APIs are not three competing technologies. They are three envelopes around the same thing: an agent invoking a tool. |
| 17 | 16 | ||
| @@ -45,19 +44,7 @@ There are two independent choices hiding in every setup: which interface you use · 27 unmodified lines | |||
| 45 | 44 | ||
| 46 | The number follows the loading axis. A CLI feels cheap because no one would ever dream of inlining the help text for every `gh` subcommand up front — the model runs `--help` for the two commands it needs, when it needs them. Then we built MCP clients that do exactly the thing nobody would do to a CLI, and blamed the protocol for the result. | 45 | The number follows the loading axis. A CLI feels cheap because no one would ever dream of inlining the help text for every `gh` subcommand up front — the model runs `--help` for the two commands it needs, when it needs them. Then we built MCP clients that do exactly the thing nobody would do to a CLI, and blamed the protocol for the result. |
| 47 | 46 | ||
| 48 | This is not a hypothetical; it is in the paper's own data. Against the same 44-tool GitHub server, the harnesses that resent the full catalog with every request spent a median of 216,986 tokens per completed run. The one harness that exposed a two-tool gateway — list tools, invoke tool — and fetched descriptions on demand spent 70,836. A 3.1x difference the authors themselves call "a property of the agent scaffolding rather than of the protocol." They also cite [Anthropic's write-up on code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) as establishing that eager transmission "is a design choice rather than a requirement of the protocol." | 47 | The good clients already work this way. Lazy loading, tool search, and code mode all defer tool descriptions until the model actually needs them — [Anthropic's write-up on code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) describes cutting token use dramatically by having the model write code that calls tools instead of front-loading every definition. Eager transmission is a client design choice, not a requirement of the protocol. Any benchmark that measures an eager client and reports the result as a property of MCP is measuring the client. |
| 49 | |||
| 50 | ## What the paper actually measured | ||
| 51 | |||
| 52 | So where did 5 to 28x come from? Here is the chart behind the headline: seven harnesses, the same GitHub task, median input tokens per completed run. Before reading on, answer the question under the chart. | ||
| 53 | |||
| 54 | <HarnessSpreadDemo client:visible /> | ||
| 55 | |||
| 56 | The 28x is a comparison between harnesses with **no MCP server attached to any run**. That is not my gotcha — it is the abstract's own phrasing: the two lean harnesses were "5.0× to 28× cheaper than the five scaffoldings that do support MCP, comparing CLI runs alone with no MCP server attached anywhere." Claude Code starts around 22,000 tokens before the task says anything, because it ships 27 built-in tool schemas with every request. pi finishes the _entire task_ in 14,660. That spread — the headline number — is pure harness overhead, measured in a room MCP never entered. | ||
| 57 | |||
| 58 | When the authors did attach the server, four of five harnesses got _cheaper_, the median paired ratio was 0.93, and both arms failed the same number of times. Their own stated noise floor is a factor of two: anything smaller "cannot be distinguished from run-to-run variation." | ||
| 59 | |||
| 60 | To be fair to the authors, they know this — the paper is literally titled _The Scaffolding Matters More Than the Interface_. My complaints are that it tests one task against one server, nearly every cell is a single run, and it still got passed around as "MCP costs 28x more." The measured content of the paper is one of the better arguments for the thesis of this post. | ||
| 61 | 48 | ||
| 62 | ## The restart is not in the spec | 49 | ## The restart is not in the spec |
| 63 | 50 | ||
| @@ -91,4 +78,4 @@ Stop arguing about envelopes. Fix the client. · 27 unmodified lines | |||
| 91 | 78 | ||
| 92 | --- | 79 | --- |
| 93 | 80 | ||
| 94 | _Numbers in this post are from [arXiv:2608.08654](https://arxiv.org/abs/2608.08654), Tables 2, 3, and 5: median input tokens per completed run. Token figures in the loading-strategy widget are illustrative, anchored to the paper's measurements (~800 tokens per tool schema, 44 tools on the official GitHub MCP server)._ | 81 | _Token figures in the loading-strategy widget are illustrative: roughly 800 tokens per tool schema, against the official GitHub MCP server's catalog of 44 tools._ |
| @@ -2080,147 +2080,6 @@ body { · 2079 unmodified lines | |||
| 2080 | white-space: nowrap; | 2080 | white-space: nowrap; |
| 2081 | } | 2081 | } |
| 2082 | 2082 | ||
| 2083 | /* ── Harness spread bars + paired ratios ── */ | ||
| 2084 | .bw-bars { | ||
| 2085 | display: flex; | ||
| 2086 | flex-direction: column; | ||
| 2087 | gap: 0.45rem; | ||
| 2088 | } | ||
| 2089 | .bw-bars__row { | ||
| 2090 | display: grid; | ||
| 2091 | grid-template-columns: 92px 1fr 64px; | ||
| 2092 | align-items: center; | ||
| 2093 | gap: 0.6rem; | ||
| 2094 | } | ||
| 2095 | .bw-bars__name { | ||
| 2096 | font-family: var(--font-mono); | ||
| 2097 | font-size: 11.5px; | ||
| 2098 | color: var(--color-ink); | ||
| 2099 | overflow: hidden; | ||
| 2100 | text-overflow: ellipsis; | ||
| 2101 | white-space: nowrap; | ||
| 2102 | } | ||
| 2103 | .bw-bars__track { | ||
| 2104 | height: 13px; | ||
| 2105 | background: var(--color-surface-2); | ||
| 2106 | border-radius: 3px; | ||
| 2107 | overflow: hidden; | ||
| 2108 | } | ||
| 2109 | .bw-bars__fill { | ||
| 2110 | height: 100%; | ||
| 2111 | background: rgba(13, 13, 16, 0.55); | ||
| 2112 | border-radius: 3px; | ||
| 2113 | } | ||
| 2114 | .bw-bars__val { | ||
| 2115 | font-family: var(--font-mono); | ||
| 2116 | font-size: 11px; | ||
| 2117 | font-variant-numeric: tabular-nums; | ||
| 2118 | color: var(--color-ink-3); | ||
| 2119 | text-align: right; | ||
| 2120 | white-space: nowrap; | ||
| 2121 | } | ||
| 2122 | .bw-quiz { | ||
| 2123 | display: flex; | ||
| 2124 | align-items: center; | ||
| 2125 | justify-content: space-between; | ||
| 2126 | gap: 0.9rem; | ||
| 2127 | flex-wrap: wrap; | ||
| 2128 | margin-top: 1rem; | ||
| 2129 | padding-top: 0.85rem; | ||
| 2130 | border-top: 1px solid var(--color-rule); | ||
| 2131 | font-size: 13.5px; | ||
| 2132 | color: var(--color-ink-2); | ||
| 2133 | } | ||
| 2134 | .bw-quiz--answer { | ||
| 2135 | color: var(--color-ink-2); | ||
| 2136 | } | ||
| 2137 | .bw-quiz strong { | ||
| 2138 | color: var(--color-ink); | ||
| 2139 | font-weight: 600; | ||
| 2140 | } | ||
| 2141 | .bw-quiz > span { | ||
| 2142 | flex: 1; | ||
| 2143 | min-width: 240px; | ||
| 2144 | } | ||
| 2145 | .bw-ratio { | ||
| 2146 | display: flex; | ||
| 2147 | flex-direction: column; | ||
| 2148 | gap: 0.55rem; | ||
| 2149 | } | ||
| 2150 | .bw-ratio__row { | ||
| 2151 | display: grid; | ||
| 2152 | grid-template-columns: 150px 1fr; | ||
| 2153 | align-items: center; | ||
| 2154 | gap: 0.6rem; | ||
| 2155 | } | ||
| 2156 | .bw-ratio__row--axis { | ||
| 2157 | margin-bottom: 0.1rem; | ||
| 2158 | } | ||
| 2159 | .bw-ratio__name { | ||
| 2160 | font-family: var(--font-mono); | ||
| 2161 | font-size: 11.5px; | ||
| 2162 | color: var(--color-ink); | ||
| 2163 | white-space: nowrap; | ||
| 2164 | overflow: hidden; | ||
| 2165 | text-overflow: ellipsis; | ||
| 2166 | } | ||
| 2167 | .bw-ratio__val { | ||
| 2168 | color: var(--color-ink-3); | ||
| 2169 | } | ||
| 2170 | .bw-ratio__axis { | ||
| 2171 | position: relative; | ||
| 2172 | height: 14px; | ||
| 2173 | } | ||
| 2174 | .bw-ratio__axis span { | ||
| 2175 | position: absolute; | ||
| 2176 | transform: translateX(-50%); | ||
| 2177 | font-family: var(--font-mono); | ||
| 2178 | font-size: 10px; | ||
| 2179 | color: var(--color-ink-3); | ||
| 2180 | white-space: nowrap; | ||
| 2181 | } | ||
| 2182 | .bw-ratio__track { | ||
| 2183 | position: relative; | ||
| 2184 | height: 18px; | ||
| 2185 | background: var(--color-surface-2); | ||
| 2186 | border-radius: 3px; | ||
| 2187 | } | ||
| 2188 | /* The 1× line every track shares, positioned by --one-x on .bw-ratio. */ | ||
| 2189 | .bw-ratio__track::after { | ||
| 2190 | content: ""; | ||
| 2191 | position: absolute; | ||
| 2192 | top: -2px; | ||
| 2193 | bottom: -2px; | ||
| 2194 | left: var(--one-x); | ||
| 2195 | width: 1px; | ||
| 2196 | background: var(--color-rule-strong); | ||
| 2197 | } | ||
| 2198 | .bw-ratio__marker { | ||
| 2199 | position: absolute; | ||
| 2200 | top: 50%; | ||
| 2201 | transform: translate(-50%, -50%); | ||
| 2202 | width: 9px; | ||
| 2203 | height: 9px; | ||
| 2204 | border-radius: 999px; | ||
| 2205 | background: white; | ||
| 2206 | border: 2px solid var(--color-ink-3); | ||
| 2207 | z-index: 1; | ||
| 2208 | } | ||
| 2209 | .bw-ratio__marker[data-cheaper="true"] { | ||
| 2210 | background: var(--color-ink); | ||
| 2211 | border-color: var(--color-ink); | ||
| 2212 | } | ||
| 2213 | .bw-ratio__legend { | ||
| 2214 | display: flex; | ||
| 2215 | justify-content: space-between; | ||
| 2216 | font-family: var(--font-mono); | ||
| 2217 | font-size: 10px; | ||
| 2218 | text-transform: uppercase; | ||
| 2219 | letter-spacing: 0.06em; | ||
| 2220 | color: var(--color-ink-3); | ||
| 2221 | margin-top: 0.2rem; | ||
| 2222 | } | ||
| 2223 | |||
| 2224 | /* ── Install race lanes ── */ | 2083 | /* ── Install race lanes ── */ |
| 2225 | .bw-lanes { | 2084 | .bw-lanes { |
| 2226 | display: grid; | 2085 | display: grid; |