Skip to content

Commit 90f7965

Browse files
committed
marketing: interactive blog post on MCP/CLI/API tool calling
1 parent c11bef2 commit 90f7965

13 files changed

Lines changed: 1683 additions & 28 deletions

apps/marketing/astro.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { unstable_readConfig } from "wrangler";
55

66
import tailwindcss from "@tailwindcss/vite";
77
import react from "@astrojs/react";
8+
import mdx from "@astrojs/mdx";
89

910
import cloudflare from "@astrojs/cloudflare";
1011

@@ -29,7 +30,7 @@ const wranglerPublicDefine = () => {
2930
export default defineConfig({
3031
site: "https://executor.sh",
3132
output: "server",
32-
integrations: [react()],
33+
integrations: [react(), mdx()],
3334
vite: {
3435
plugins: [tailwindcss()],
3536
define: wranglerPublicDefine(),

apps/marketing/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
},
1616
"dependencies": {
1717
"@astrojs/cloudflare": "^13.0.0",
18+
"@astrojs/mdx": "^6.0.3",
1819
"@astrojs/react": "^5.0.4",
1920
"@executor-js/react": "workspace:*",
2021
"@tailwindcss/vite": "^4.2.2",
21-
"astro": "^6.1.3",
22+
"astro": "^6.4.8",
2223
"clsx": "^2.1.1",
2324
"motion": "^12.38.0",
2425
"posthog-js": "^1.372.5",

apps/marketing/public/llms.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ Run it your way: local CLI, a desktop app, hosted Executor Cloud, or self-hosted
2323
- [Pricing](https://executor.sh/#pricing): plans for Executor Cloud.
2424
- [Install](https://executor.sh/#install): install the CLI and connect your first agent.
2525

26+
## Blog
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.
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+
2631
## Source
2732

2833
- [GitHub](https://github.com/UsefulSoftwareCo/executor): source for the integration layer, plugins, and hosts.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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, useAnimatedNumber } from "./shared";
8+
9+
/**
10+
* The centerpiece: context cost is a 2×2, not a protocol property. One axis
11+
* picks the interface (CLI or MCP), the other picks the loading strategy
12+
* (on demand or everything up front). Flipping the interface barely moves the
13+
* number; flipping the loading strategy moves it ~20x.
14+
*
15+
* Figures are illustrative but anchored: the arXiv paper everyone cited
16+
* measured the official GitHub MCP server at 44 tools, eager clients resending
17+
* the whole catalog every request, and ~22k tokens for 27 built-in tool
18+
* schemas (~800 tokens per schema).
19+
*/
20+
21+
type Interface_ = "cli" | "mcp";
22+
type Loading = "lazy" | "eager";
23+
24+
const SYSTEM_TOK = 1200;
25+
26+
type Segment = { readonly label: string; readonly tok: number; readonly flood?: boolean };
27+
28+
const CELLS: Record<Interface_, Record<Loading, ReadonlyArray<Segment>>> = {
29+
cli: {
30+
lazy: [{ label: "one bash tool definition", tok: 320 }],
31+
eager: [
32+
{ label: "one bash tool definition", tok: 320 },
33+
{ label: "--help for every gh subcommand, inlined", tok: 29800, flood: true },
34+
],
35+
},
36+
mcp: {
37+
lazy: [{ label: "gateway pair: search tools + invoke tool", tok: 640 }],
38+
eager: [{ label: "44 tool schemas × ~800 tokens", tok: 35200, flood: true }],
39+
},
40+
};
41+
42+
const cellTotal = (i: Interface_, l: Loading) =>
43+
SYSTEM_TOK + CELLS[i][l].reduce((s, seg) => s + seg.tok, 0);
44+
45+
const MAX_TOTAL = Math.max(
46+
cellTotal("cli", "eager"),
47+
cellTotal("mcp", "eager"),
48+
cellTotal("cli", "lazy"),
49+
cellTotal("mcp", "lazy"),
50+
);
51+
52+
function Seg({
53+
value,
54+
onPick,
55+
options,
56+
label,
57+
}: {
58+
readonly value: string;
59+
readonly onPick: (v: string) => void;
60+
readonly options: ReadonlyArray<{ readonly id: string; readonly label: string }>;
61+
readonly label: string;
62+
}) {
63+
return (
64+
<div className="bw-axis">
65+
<span className="bw-axis__label">{label}</span>
66+
<div className="bw-seg" role="group" aria-label={label}>
67+
{options.map((o) => (
68+
<button
69+
key={o.id}
70+
type="button"
71+
className="bw-seg__btn"
72+
data-on={value === o.id ? "true" : undefined}
73+
aria-pressed={value === o.id}
74+
onClick={() => onPick(o.id)}
75+
>
76+
{o.label}
77+
</button>
78+
))}
79+
</div>
80+
</div>
81+
);
82+
}
83+
84+
export function ContextFloodDemo() {
85+
const [iface, setIface] = useState<Interface_>("mcp");
86+
const [loading, setLoading] = useState<Loading>("eager");
87+
88+
const segments: ReadonlyArray<Segment> = [
89+
{ label: "system prompt", tok: SYSTEM_TOK },
90+
...CELLS[iface][loading],
91+
];
92+
const total = cellTotal(iface, loading);
93+
const totalDisplay = useAnimatedNumber(total);
94+
const flooding = segments.some((s) => s.flood);
95+
96+
return (
97+
<div className="bw">
98+
<p className="sr-only" aria-live="polite">
99+
{iface === "cli" ? "CLI" : "MCP"} with tools loaded{" "}
100+
{loading === "lazy" ? "on demand" : "up front"}: about {fmt(total)} tokens spent before your
101+
first message.
102+
</p>
103+
104+
<div className="bw-head bw-head--stack">
105+
<Seg
106+
label="Interface"
107+
value={iface}
108+
onPick={(v) => setIface(v as Interface_)}
109+
options={[
110+
{ id: "cli", label: "CLI" },
111+
{ id: "mcp", label: "MCP server" },
112+
]}
113+
/>
114+
<Seg
115+
label="Tool loading"
116+
value={loading}
117+
onPick={(v) => setLoading(v as Loading)}
118+
options={[
119+
{ id: "lazy", label: "On demand" },
120+
{ id: "eager", label: "Everything up front" },
121+
]}
122+
/>
123+
</div>
124+
125+
<div className="bw-flood">
126+
<div className="bw-flood__rows">
127+
{segments.map((s) => (
128+
<div key={s.label} className="bw-flood__row">
129+
<div className="bw-flood__meta">
130+
<span className="bw-flood__name">{s.label}</span>
131+
<span className="bw-flood__tok">~{fmt(s.tok)} tok</span>
132+
</div>
133+
<div className="bw-flood__track">
134+
<div
135+
className="bw-flood__fill"
136+
data-flood={s.flood ? "true" : undefined}
137+
style={{ width: `${Math.max(1.5, (s.tok / MAX_TOTAL) * 100)}%` }}
138+
/>
139+
</div>
140+
</div>
141+
))}
142+
</div>
143+
<div className="bw-flood__total">
144+
<div className="bw-flood__num">~{fmt(totalDisplay)}</div>
145+
<div className="bw-flood__cap">tokens before your first message</div>
146+
<div className="bw-badge" data-show={flooding ? "true" : undefined}>
147+
resent with every request
148+
</div>
149+
</div>
150+
</div>
151+
152+
<div className="bw-map" role="group" aria-label="All four combinations">
153+
{(["cli", "mcp"] as const).flatMap((i) =>
154+
(["lazy", "eager"] as const).map((l) => (
155+
<button
156+
key={`${i}-${l}`}
157+
type="button"
158+
className="bw-map__cell"
159+
data-on={i === iface && l === loading ? "true" : undefined}
160+
onClick={() => {
161+
setIface(i);
162+
setLoading(l);
163+
}}
164+
>
165+
<span className="bw-map__label">
166+
{i === "cli" ? "CLI" : "MCP"} · {l === "lazy" ? "on demand" : "up front"}
167+
</span>
168+
<span className="bw-map__val">~{fmt(cellTotal(i, l))}</span>
169+
</button>
170+
)),
171+
)}
172+
</div>
173+
174+
<div className="bw-foot">
175+
Flip the interface: the number barely moves. Flip the loading strategy: ~20x. The bloat
176+
lives on one axis, and it is not the protocol axis.
177+
</div>
178+
</div>
179+
);
180+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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)}%` }}></span>
117+
<span style={{ left: `${logPos(2)}%` }}></span>
118+
<span style={{ left: `${logPos(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+
}

0 commit comments

Comments
 (0)