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
8 changes: 8 additions & 0 deletions .github/workflows/brand-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ jobs:
# keeping and a dated name per run only accumulates orphans.
BR="brand-sync"
git push -f origin "HEAD:${BR}"
# The PR a human opened last time is still open: the force-push just
# refreshed it, and `gh pr create` would fail with "already exists" —
# which the fallback below used to report as "PR creation is
# unavailable, open it by hand" (round 4b).
if EXISTING=$(gh pr list --head "$BR" --state open --json number -q '.[0].number') && [ -n "$EXISTING" ]; then
echo "refreshed the open PR #${EXISTING} from ${BR}"
exit 0
fi
if gh pr create --head "$BR" \
--title "chore: sync brand numbers" \
--body "Automated marker refresh from https://blockrun.ai/brand/numbers.json (scripts/sync-brand-numbers.mjs --refresh). Opened as a PR because the default branch is protected."; then
Expand Down
32 changes: 29 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ concurrency:

jobs:
publish:
# main only, for workflow_dispatch as much as for push: the dispatch form
# accepts any ref, and every guard below is ref-agnostic (the version gate
# only ORDERS pkg against npm), so a write collaborator could have shipped
# a feature branch as `latest` — tagged at an unmerged commit that never
# passed the required check (round 4b).
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write # required to push the tag + create the GitHub release
Expand Down Expand Up @@ -67,8 +73,16 @@ jobs:
#
# With the parameter the endpoint returns exactly one row, the real
# latest, isLatest=true.
REG=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.BlockRunAI/blockrun-mcp&version=latest" \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{const j=JSON.parse(s);const a=j.servers||[];console.log(a[0]?.server?.version||'none')}catch{console.log('none')}})")
# Same none/unknown split as npm above: "none" only when the registry
# ANSWERED and lists no server; a non-2xx (curl -f), an unparsable
# body or a missing `servers` field is "unknown" and stops the job
# before mcp-publisher runs blind (round 4b).
if REG_BODY=$(curl -fsS "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.BlockRunAI/blockrun-mcp&version=latest"); then
REG=$(printf '%s' "$REG_BODY" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{const j=JSON.parse(s);if(!Array.isArray(j.servers)){console.log('unknown');return}console.log(j.servers[0]?.server?.version||'none')}catch{console.log('unknown')}})")
else
echo "::warning::registry lookup failed (non-2xx or network) — treating as unknown"
REG=unknown
fi
# The tag is the third target, and it needs its OWN guard computed
# here, not derived from the npm one. Ask the remote: actions/checkout
# fetches no tags, so a local lookup always says "missing".
Expand All @@ -91,6 +105,12 @@ jobs:
- name: Refuse a version below npm latest
run: node scripts/version-gate.mjs "${{ steps.v.outputs.pkg }}" "${{ steps.v.outputs.npm }}"

- name: Refuse to run against an unknown registry state
if: steps.v.outputs.reg == 'unknown'
run: |
echo "::error::the MCP registry could not be read, so whether ${{ steps.v.outputs.pkg }} is already there is unknown — re-run the job"
exit 1

- name: Install dependencies
run: npm ci

Expand Down Expand Up @@ -178,10 +198,16 @@ jobs:
# whose build or npm publish FAILED does not tag either: `!cancelled()`
# lets this step run past a registry failure, and the outcome checks
# keep it from running past a real one.
#
# NOT gated on `tag_missing`: once `git push origin $TAG` has succeeded,
# a re-run after `gh release create` failed resolves tag_missing=false
# and would skip the step — the in-step `gh release view` guard is what
# makes THAT re-run safe, and it can only run if the step runs. The
# `git ls-remote` guard inside keeps a stale HEAD from being re-tagged;
# tag_missing is still computed in Resolve for the log (round 4b).
- name: Tag + GitHub release
if: >-
!cancelled()
&& steps.v.outputs.tag_missing == 'true'
&& steps.build.outcome == 'success'
&& (steps.npm.outcome == 'success' || steps.v.outputs.pkg == steps.v.outputs.npm)
env:
Expand Down
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,93 @@

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

## 0.51.1

**Round four, second half.** 0.51.0 shipped with seven of round four's ten
finders still unread — they had stalled — and this is what they found once
they finished, plus a regression hunt over 0.51.0's own fixes. Forty-six
findings survived two verifiers each: two P0, five P1, and three of the P1s
were introduced by 0.51.0. That is the pattern every round has produced, and
it is why the rounds continue.

### The money paths

- **Two more ways the URL parser and the price classifier disagreed** (P0).
The parser strips trailing control-or-space from the whole URL — and the
slug is its tail on every rail — so `phone/numbers/buy ` (an ordinary
tokenisation slip) reserved $0.012 and bought the $5.001 route. And for
https the parser reads a literal `\` as `/`, so `sandbox\create` priced as
a $0.003 op, skipped the gpu/timeout normalisation, and the gateway served a
$192 sandbox. Both guards share the cut now.
- **0.51.0's settled-then-failed evidence was reading a shared counter** (P1).
The SDK's spend counter is per client, `getClient()` was a cached singleton
per rail, and tool calls run concurrently — so a concurrent call's
settlement landed inside a failing call's window and was booked to it as
"the charge stands", then booked again by the call that paid. Every path
tool builds its own client, as chat always has.
- **An edge status is a maybe on every rail** (P1). 0.51.0 read every numeric
status as the gateway's verdict, which undid, for the media tools alone, the
rule chat and the path tools apply to the same status: 408/502/504/52x say
only that the origin did not answer in time and may still be settling. A
`blockrun_speech` answered 504 by the edge while TTS finished and billed at
the origin read "failed — try again", i.e. pay twice. One shared set now,
and the tracker remembers that a paid request LEFT even after a response
settled it.
- **0.51.0's placeholder claim could delete a peer's freshly published key**
(P1). The claim renamed whatever was at the session file's name after a read
that saw it empty; a peer that claimed the same placeholder and linked its
key in the gap had it renamed aside and deleted. The aside is inspected and a
key found there is adopted.
- **A DNS failure on the native claude-* path booked the quote and forbade a
retry** (P1). The Anthropic SDK nests undici's errno two causes deep; the
classifier read one. The chain is walked; a connect timeout is
never-connected too. An idle stall before the stream connects is a maybe,
not "settled"; partial text survives on the native path; and the Solana
frame path refuses a stream that ended with no frames instead of returning
a paid, empty success.
- **Only the POST of a signed Polymarket order can be unknown.** The SDK's
helpers read the network before they sign, and a relay 502 on any of those
reads was wrapped as a phantom "possibly live" order that never released.
The creds retry never re-submits an unknown outcome; the reservation is
taken before the spend dialog so two waiting confirms cannot overshoot the
session cap; the EOA withdraw gets the double-send guard the relayer path
had; a throw from the relayer's poll is "may still land"; an unreadable
state file is a refusal, not an empty state; and every definite refusal
says nothing was placed, so the order card re-arms on it.
- **The Linux keychain fix locked out fresh installs with no secrets service**
(a regression of 0.51.0). secret-tool prints a D-Bus or no-provider message
and exits 1 on hosts that have the binary and nothing behind it; that read
as a fault, and every paid tool refused to mint. A keychain that does not
exist is absent; a locked one is still an error.
- `blockrun_image` and `blockrun_realface` say when the charge stands (a
settled 2xx with no URL / no asset id, a temp file that would not write); the
account rail's unreadable settled body is the same typed error the wallet
rails throw; `BASE_RPC_URL` is honoured on the Base balance read.

### The release machinery

`publish.yml` runs on `main` only — a `workflow_dispatch` from a bumped branch
would have shipped it as `latest`, tagged at an unmerged commit. The
tag/release step is no longer gated on the tag being absent, so a re-run after
`gh release create` failed can actually create the release (the very failure
that hit 0.51.0's first run). The MCP-registry lookup gets the npm lookup's
none/unknown split. `brand-sync` refreshes an already-open fallback PR instead
of going red. `verify:prices` tallies a probe that throws as unreachable
(bounded by 30s) instead of exiting with the under-reserve code, and
`version-gate.mjs` realpaths its `isMain` check so a symlinked checkout cannot
fail open.

### Saying the true thing

"Timeout = no charge, retry" was wrong on Solana music and every account-rail
media job; the setup and debug skills told the user to back up only
`.session` when the funded key on a new install is `.solana-session`;
spend-confirmation.md said Polymarket is not behind the dialog; four docs said
native claude-* is Base-only when only `thinking` is; the gentech skill said
music, speech and realface need Base; CONTRIBUTING's async pattern said
timeouts never charge. All corrected, and the music/video tool descriptions
say which rails bill at submit.

## 0.51.0

**Round four went at round three.** 0.50.0's changelog said the general sweep
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Two flavors. Pick the right one:
- **Sync, single-call**: use `rawGet(client, endpoint, params)` (GET) or `rawPost(client, endpoint, body)` (POST) from `src/utils/raw-call.ts`. See `src/tools/markets.ts`, `src/tools/exa.ts`.

Do **not** reach for `client.getWithPaymentRaw` / `client.requestWithPaymentRaw` directly. There are three payment rails (Base wallet, Solana wallet, account API key) and the SDK only knows about the two wallet ones — on the account rail it degrades to a plain Bearer fetch and throws away the `x-blockrun-cost-usd` header, so the call cannot report what it cost. `raw-call.ts` exists so no tool picks a rail for itself, and book the ledger with its `ledgerFallback()` rather than the reserved amount: the gate and the ledger are deliberately different numbers, and Solana has no gateway transaction fee at all.
- **Async, payment-on-completion**: copy the `src/tools/video.ts` pattern — submit → 402 → sign → poll the same URL with the same `PAYMENT-SIGNATURE` header → settle on the first `completed` response. Upstream failures or client-side timeout = no charge. See `src/tools/music.ts` for the simpler synchronous-blocking variant.
- **Async**: copy the `src/tools/video.ts` pattern — submit → 402 → sign → poll the same URL with the same `PAYMENT-SIGNATURE` header → settle on the first `completed` response, with the catch classifying by FACT (a booked settlement, a typed `BilledJobError` / `JobFailedError`, the in-flight tracker), never by the words in the message. Whether a give-up cost anything is per rail: the Base wallet settles on completion (upstream failure or client timeout = no charge); Solana music and every account-rail media job are billed at submit, and `utils/solana-402.ts` / `utils/api-key-call.ts` hand you a `BilledJobError` for those. `src/tools/music.ts` is the same shape (its submit can also settle inline), and `src/tools/speech.ts` the single-POST one.

## CHANGELOG

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ claude mcp add blockrun -s user -- npx -y @blockrun/mcp@latest
<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: 13.0K tokens, 7% of a 200K context window, charged every turn whether or not you call a tool. 5.4K with --profile trading, 58% less.">
<img src="assets/context-cost.svg" width="620" alt="Context cost: 13.0K tokens, 7% of a 200K context window, charged every turn whether or not you call a tool. 5.4K with --profile trading, 59% less.">
</picture>
</div>

Expand Down Expand Up @@ -252,13 +252,13 @@ Package managers have shown install size for decades. Almost no MCP server shows

| Profile | Tools | Context |
|---------|-------|---------|
| `full` *(default)* | 19 | 13,006 |
| `full` *(default)* | 19 | 13,044 |
| `trading` | 8 | 5,411 |
| `media` | 7 | 5,790 |
| `media` | 7 | 5,828 |
| `research` | 5 | 2,752 |
| `chat` | 3 | 2,079 |

Running `--profile trading` instead of the default costs **58% less context** for the same trading
Running `--profile trading` instead of the default costs **59% less context** for the same trading
workflow. If you only ever ask about markets, that is the single cheapest change you can make.

Measure it yourself — against us, or against any other stdio MCP server:
Expand Down Expand Up @@ -611,11 +611,11 @@ One wallet, or one dashboard-backed API key. All sources.
| `BLOCKRUN_API_KEY` | unset | A BlockRun account key (`brk_live_…`) from [user.blockrun.ai/dashboard/keys](https://user.blockrun.ai/dashboard/keys). **Set → account billing through `api.blockrun.ai`: no wallet is created, read or used, and no chain applies.** Takes priority over every wallet setting below. A malformed value is a startup error, never a silent fall back to the wallet. |
| `~/.blockrun/.api-key` | not created | The same key on disk, for clients that make env vars awkward. Read only when `BLOCKRUN_API_KEY` is unset; an empty or unreadable file falls through to wallet mode. |
| `BLOCKRUN_API_BASE_URL` | `https://api.blockrun.ai` | Account API service endpoint used after you get a key at `user.blockrun.ai`. Override only for staging. Accepts the OpenAI-style `…/v1` form too. |
| `~/.blockrun/.session` | auto-created on first run | EVM private key (0x…). File exists → use Base. Also the Polymarket signer (unless `BLOCKRUN_WALLET_KEY` or an agent `wallet.json` takes precedence). |
| `~/.blockrun/.session` | created on the first `blockrun_wallet` call (with `.solana-session`) | EVM private key (0x…). Which chain pays is the priority list below, not this file's existence. Also the Polymarket signer (unless `BLOCKRUN_WALLET_KEY` or an agent `wallet.json` takes precedence). |
| `BLOCKRUN_WALLET_KEY` | unset | Env override of the EVM key — takes precedence over `.session` / `wallet.json` as the Base + Polymarket signer. |
| `~/.blockrun/.chain` | unset | Explicit chain preference: `base` or `solana`. Written only by `blockrun_wallet action:"chain"` — i.e. only when you choose. |
| `~/.blockrun/.chain-auto` | written on first run | Automatic pin: the chain you were already on when your second wallet was provisioned. Keeps a Base user on Base once a Solana session exists, and is outranked by `SOLANA_WALLET_KEY`. Cleared whenever you set a chain explicitly. |
| `~/.blockrun/.solana-session` | not created | Solana private key. File exists → Solana unless `.chain` says `base`. |
| `~/.blockrun/.solana-session` | created on the first `blockrun_wallet` call (with `.session`) | Solana private key — the funded wallet on a new install. Which chain pays is the priority list below. |
| `SOLANA_WALLET_KEY` | unset | Env override of `.solana-session`. Set → use Solana. |
| `BLOCKRUN_KEYCHAIN` | `auto` | Key storage. `auto` — mirror the key into the OS keychain (macOS Keychain / Linux `secret-tool`) and keep the plaintext file, which stays authoritative so other BlockRun tools keep working and so replacing it still rotates your wallet. `off` — file only. `strict` — also delete `~/.blockrun/.session` once a read-back proves the keychain holds the same key; **this breaks other tools that read that file directly**. |
| `BLOCKRUN_MCP_PROFILE` | `full` | Tool profile (`media` / `trading` / `research` / `chat`). |
Expand Down Expand Up @@ -653,7 +653,7 @@ The server runs a non-blocking npm registry check at startup and prints an `Upda
Then restart Claude Code. Or pin absolute paths (`which npx`).
- **`claude mcp list` doesn't show `blockrun`** → Check `node -v` (≥20.19). Clear the npx cache: `rm -rf ~/.npm/_npx`. Re-run the install.
- **`fetch failed` / balance-check timeout** → Base RPC transient outage. The tool falls through 3 public RPCs; retry after 30s. Persistent = local proxy / firewall blocking outbound RPC.
- **`Video`/`Music generation timed out`** → Upstream queue congestion. **No charge** (payment-on-completion). Retry, or pick a faster model.
- **`Video`/`Music generation timed out`** → Upstream queue congestion. Whether it cost anything depends on the rail, and the error says which: on the **Base wallet** payment settles on completion, so it is **not charged** and a retry is safe; on **Solana music** and **every account-rail media job** the gateway bills at submit, so the job **is charged**, the error names it, and re-running bills a second one — check `blockrun_wallet action:"report"` instead.
- **`blockrun_price` says `Equity quotes are not served (gateway 501 …)`** → Equity price/history were withdrawn on 2026-09-05; not an outage, and **nothing was charged** (the wallet is never asked to sign). The ticker catalog (`action:"list" category:"stocks"`) is still free. Equity coverage: hello@blockrun.ai.
- **`blockrun_markets` on `sports/*` fails — before 0.49.0 as `API error after payment: 502` with no balance change** → Predexon's `sports/*` routes have been down upstream since 2026-08-04; the gateway releases the payment on that upstream 500, so the call is **not charged** (the error says so when the gateway's "payment NOT charged" confirmation is in the response; otherwise it tells you to check `blockrun_wallet action:"report"`). For sports odds use `path:"markets/search"` with `params:{ q: "NBA" }`, or `polymarket/events` with `params:{ search: "NBA" }` — the bare `markets` route and its `league` filter were removed upstream on 2026-08-04 and 404 before payment. Upgrade to ≥ 0.49.0 so the error says all of this itself.
- **No spend-confirmation dialog although `BLOCKRUN_CONFIRM_SPEND=on`** → Your client doesn't support MCP elicitation (Windsurf, Codex, Gemini CLI); the server proceeds without asking by design. Use `BLOCKRUN_BUDGET_LIMIT` as the guard, or a client from the [support table](#%EF%B8%8F-human-in-the-loop-payments).
Expand Down Expand Up @@ -693,7 +693,7 @@ Yes — `BLOCKRUN_CONFIRM_SPEND=on`. Every paid tool pauses with the estimated c
Yes. `blockrun_polymarket` places real, USDC-settled orders on Polymarket's CLOB — confirm-gated and capped. Read the odds with `blockrun_markets`, place with `blockrun_polymarket`.

**Base or Solana?**
Both. Switch instantly with `blockrun_wallet action:"chain"`. Three things are Base-only, and each says so when you call them on Solana: `blockrun_defi` (DefiLlama) and `blockrun_modal`, which the Solana gateway does not serve, and native Anthropic `claude-*` chat. Media generation, markets, search and Polymarket all settle on either chain.
Both. Switch instantly with `blockrun_wallet action:"chain"`. Three things are Base-only, and each says so when you call them on Solana: `blockrun_defi` (DefiLlama) and `blockrun_modal`, which the Solana gateway does not serve, and `claude-*` chat with `thinking` (the native `/v1/messages` path pays on Base; a plain `claude-*` call takes the compat path on Solana). Media generation, markets, search and Polymarket all settle on either chain.

---

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.51.0
0.51.1
Loading
Loading