Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to BlockRun MCP will be documented in this file.

## 0.45.1

**The context number gets its own card instead of being the second of nineteen
badges.** 0.45.0 published the figure but buried it — a shields badge in a row
with `MIT License`, `TypeScript 5` and `Node ≥20.19`, which are table stakes,
next to the one number nothing else in the ecosystem publishes. It now sits
directly under the install command as a card: the figure large, a reference
unit that makes it mean something (6% of a 200K context window), the
`--profile trading` alternative at −57%, and the line that matters — *every
turn, whether or not you call a tool*.

The card is generated by `npm run brand:card`, never drawn by hand, so it
cannot outlive the number it states; `test/schema-tokens.test.ts` checks both
the light and dark variants against a live measurement and fails the build if
either drifts. Light and dark ship as separate SVGs behind `<picture>`, because
an SVG cannot restyle itself from the host page.

## 0.45.0

**Every tool schema got smaller, and the README now tells you by how much.**
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Agents can only sign transactions.<br><br>
<br>

<img src="https://img.shields.io/badge/🧰_20_Tools-success?style=for-the-badge" alt="20 tools">&nbsp;
<img src="https://img.shields.io/badge/🧮_12.9K_Context_Tokens-5B9BF6?style=for-the-badge" alt="12.9K context tokens">&nbsp;
<img src="https://img.shields.io/badge/🤖_Agent--Native-black?style=for-the-badge" alt="Agent native">&nbsp;
<img src="https://img.shields.io/badge/🔑_Zero_API_Keys-blue?style=for-the-badge" alt="No API keys">&nbsp;
<img src="https://img.shields.io/badge/📈_Read_+_Trade_Polymarket-e11d48?style=for-the-badge" alt="Read and trade Polymarket">&nbsp;
Expand Down Expand Up @@ -41,6 +40,15 @@ claude mcp add blockrun -s user -- npx -y @blockrun/mcp@latest

<div align="center"><em>Wallet auto-created on first run. Fund with $5 USDC. Ask Claude anything.</em></div>

<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/context-cost-dark.svg">
<img src="assets/context-cost.svg" width="620" alt="Context cost: 12.9K tokens, 6% of a 200K context window, charged every turn whether or not you call a tool. 5.6K with --profile trading, 57% less.">
</picture>
</div>

<div align="center"><sub>Every MCP server costs you this. Almost none of them tell you. <a href="#what-each-profile-costs-your-context">How we measure it →</a></sub></div>

---

> **BlockRun MCP** is an open-source [Model Context Protocol](https://modelcontextprotocol.io) server that gives Claude — and any MCP-compatible agent — <!-- br:mcp.tools -->20<!-- /br:mcp.tools --> tools for real-time data and real actions: <!-- br:models.chatVisible -->71<!-- /br:models.chatVisible --> LLMs, image & video generation, prediction-market data, live web/X search, on-chain queries across <!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains, and **the ability to place real, USDC-settled bets on Polymarket**. Authentication is a wallet signature (no API keys); you pay per call in USDC via the [x402](https://x402.org) protocol (no credit cards, no subscriptions). One self-custody wallet on Base or Solana. MIT licensed.
Expand Down
10 changes: 10 additions & 0 deletions assets/context-cost-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions assets/context-cost.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blockrun/mcp",
"version": "0.45.0",
"version": "0.45.1",
"mcpName": "io.github.BlockRunAI/blockrun-mcp",
"description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, and crypto data. Paid via x402 micropayments.",
"type": "module",
Expand All @@ -24,6 +24,7 @@
"prepublishOnly": "npm run build",
"verify:prices": "tsx scripts/verify-prices.ts",
"measure:schema": "node scripts/measure-tool-schema.mjs",
"brand:card": "node scripts/measure-tool-schema.mjs --svg",
"e2e:polymarket:readonly": "tsx scripts/polymarket-e2e-readonly.ts",
"e2e:polymarket:approvals": "tsx scripts/polymarket-e2e-verify-approvals.ts",
"e2e:polymarket:approve": "tsx scripts/polymarket-e2e-approve.ts",
Expand Down
47 changes: 47 additions & 0 deletions scripts/measure-tool-schema.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* percent higher on JSON, so every number here is a slight UNDER-count.
*/
import { spawn } from "node:child_process";
import { writeFileSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";
import { encode } from "gpt-tokenizer/encoding/o200k_base";

Expand Down Expand Up @@ -129,6 +130,41 @@ export function measure(tools, prefix = "") {
/** 12900 -> "12.9K". The form the README badge publishes. */
export const asK = (n) => `${(Math.round(n / 100) / 10).toFixed(1)}K`;

/* ── the context-cost card ──────────────────────────────────────────────────
*
* Generated, never hand-drawn: the figures come from the measurement above, so
* the card cannot say something the server stopped doing. `--svg` rewrites
* both variants; test/schema-tokens.test.ts fails if they drift.
*
* Two files because GitHub picks between them with <picture media=...>; an SVG
* cannot restyle itself from the host page's colour scheme. Text is left at a
* system font stack rather than converted to paths so it stays selectable and
* legible at any zoom — the tradeoff is that positions are hand-tuned with
* enough slack that a wider glyph set cannot collide.
*/
const REFERENCE_WINDOW = 200_000;

export function renderCard({ total, tradingTotal, cut, dark }) {
const c = dark
? { bg: "#0B0A0F", stroke: "#26242E", label: "#8A8797", value: "#FFFFFF", accent: "#5B9BF6", muted: "#6E6B7B" }
: { bg: "#FFFFFF", stroke: "#E4E2E8", label: "#6E6B7B", value: "#0B0A0F", accent: "#2563EB", muted: "#8A8797" };
const pct = Math.round((total / REFERENCE_WINDOW) * 100);
const sans = "-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif";
const mono = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";

return `<svg xmlns="http://www.w3.org/2000/svg" width="620" height="150" viewBox="0 0 620 150" role="img" aria-label="Context cost: ${asK(total)} tokens, ${pct}% of a 200K context window; ${asK(tradingTotal)} with --profile trading, ${cut}% less">
<defs><clipPath id="card-${dark ? "d" : "l"}"><rect x="0.5" y="0.5" width="619" height="149" rx="12"/></clipPath></defs>
<rect x="0.5" y="0.5" width="619" height="149" rx="12" fill="${c.bg}" stroke="${c.stroke}"/>
<rect x="0.5" y="0.5" width="5" height="149" fill="${c.accent}" clip-path="url(#card-${dark ? "d" : "l"})"/>
<text x="28" y="34" font-family="${sans}" font-size="11" font-weight="600" letter-spacing="1.6" fill="${c.label}">CONTEXT COST</text>
<text x="28" y="78" font-family="${sans}" font-size="34" font-weight="700" fill="${c.value}">${asK(total)} tokens</text>
<text x="28" y="103" font-family="${sans}" font-size="13" fill="${c.muted}">${pct}% of a 200K context window · every turn, whether or not you call a tool</text>
<text x="28" y="127" font-family="${sans}" font-size="13" fill="${c.label}">${asK(tradingTotal)} with <tspan font-family="${mono}" fill="${c.accent}">--profile trading</tspan> — ${cut}% less</text>
<text x="592" y="34" text-anchor="end" font-family="${mono}" font-size="10.5" fill="${c.muted}">measured, not estimated</text>
</svg>
`;
}

// Importable: test/schema-tokens.test.ts reuses measure()/asK() to pin the
// published number, so the CLI half must not run on import.
const isCli = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
Expand All @@ -143,6 +179,17 @@ for (const profile of PROFILES) {
results[profile ?? userCmd.join(" ")] = measure(await listTools(cmd), PREFIX);
}

if (flags.has("--svg")) {
const full = results.full, trading = results.trading;
if (!full || !trading) throw new Error("--svg needs the full and trading profiles (drop `-- <cmd>`)");
const cut = Math.round((1 - trading.total / full.total) * 100);
for (const dark of [false, true]) {
const file = new URL(`../assets/context-cost${dark ? "-dark" : ""}.svg`, import.meta.url);
writeFileSync(file, renderCard({ total: full.total, tradingTotal: trading.total, cut, dark }));
console.log(`wrote ${fileURLToPath(file).split("/").slice(-2).join("/")}`);
}
}

if (asJson) {
console.log(JSON.stringify(results, null, 2));
} else {
Expand Down
28 changes: 21 additions & 7 deletions test/schema-tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,28 @@ async function measureProfile(profile: string) {
};
}

test("the README badge states the measured context cost", async () => {
test("the context-cost card states the measured numbers", async () => {
// The card is generated by `npm run brand:card`, never hand-drawn, so the
// only way it can lie is if someone edits the SVG or forgets to regenerate.
// Both variants are checked: a dark-mode reader sees a different file.
const full = await measureProfile("full");
const shown = asK(full.total) as string;
assert.ok(
README.includes(`badge/🧮_${shown}_Context_Tokens`),
`badge URL should show ${shown} (measured ${full.total} tokens)`,
);
assert.ok(README.includes(`alt="${shown} context tokens"`), "badge alt text");
const trading = await measureProfile("trading");
const cut = Math.round((1 - trading.total / full.total) * 100);
const pct = Math.round((full.total / 200_000) * 100);

for (const variant of ["context-cost.svg", "context-cost-dark.svg"]) {
const svg = readFileSync(new URL(`../assets/${variant}`, import.meta.url), "utf8");
assert.ok(svg.includes(`${asK(full.total)} tokens`), `${variant}: headline figure`);
assert.ok(svg.includes(`${pct}% of a 200K context window`), `${variant}: reference unit`);
assert.ok(svg.includes(`${asK(trading.total)} with`), `${variant}: trading figure`);
assert.ok(svg.includes(`${cut}% less`), `${variant}: saving`);
}
});

test("the README shows both card variants", () => {
// Without the <source>, dark-mode readers get the light card on a dark page.
assert.ok(README.includes('srcset="assets/context-cost-dark.svg"'), "dark source");
assert.ok(README.includes('src="assets/context-cost.svg"'), "light fallback");
});

test("the profile table states each profile's measured cost", async () => {
Expand Down
Loading