Metadata-Version: 2.5
Name: langbot-mcp
Version: 0.1.0
Summary: Langbot MCP server: every market on Kalshi and Polymarket, its history and replays against recorded order books, for Claude Code, Cursor, Claude Desktop and Codex.
Project-URL: Homepage, https://langbot.ai
Project-URL: Documentation, https://langbot.ai/docs
Author-email: Langbot <hello@langbot.ai>
Maintainer-email: Langbot <hello@langbot.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: backtesting,kalshi,mcp,polymarket,prediction-markets,trading-agents
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: anthropic>=1.0
Requires-Dist: fastmcp<5,>=4
Requires-Dist: httpx>=0.27
Requires-Dist: langbot-sdk<0.2,>=0.1.0
Description-Content-Type: text/markdown

# langbot-mcp

<!-- mcp-name: ai.langbot/langbot -->

MCP server for [Langbot](https://langbot.ai), the data and simulation layer for every market on
Kalshi and Polymarket: search any market and read its price history, compare the same question
across both venues, replay past events against the order books Langbot recorded, and, for
markets that settle on what is said (mention markets are one example), the live record of the
event with verifiable receipts. Data, never trading signals or advice: the optional strategy
tools are off unless you opt in.

Over local stdio, `search_markets` and `get_market_history` work without a token.

```bash
uvx langbot-mcp                      # stdio — what Claude Code / Cursor / Claude Desktop / Codex run
langbot-mcp --http                   # streamable HTTP on 0.0.0.0:8001 at /mcp (hosted: https://mcp.langbot.ai/mcp); multi-tenant, token per request
langbot mcp install claude-code      # one-command install (also: cursor, claude-desktop, codex, all)
```

Manual client entry:

```json
{ "mcpServers": { "langbot": { "command": "uvx", "args": ["langbot-mcp"], "env": { "LANGBOT_API_TOKEN": "lb_…" } } } }
```

Hosted, for clients that send headers (Cursor, Claude Code `--transport http`):

```json
{ "mcpServers": { "langbot": { "url": "https://mcp.langbot.ai/mcp", "headers": { "Authorization": "Bearer lb_…" } } } }
```

**Auth.** Each tool's `_meta` has `langbot/auth` (`none` | `token`: what the feed needs, which is
what applies over local stdio) and `langbot/auth_hosted` (always `bearer`). The hosted endpoint
needs `Authorization: Bearer lb_…` on every tool call and resource read, including `none` tools;
without a token, run it locally (`uvx langbot-mcp`).

## Tools

| tool | what |
|---|---|
| `search_markets(query, venue?, status, category?, limit)` | every Kalshi and Polymarket market Langbot lists (no token on stdio) |
| `get_market_history(ticker, max_points=200)` | one market's status, result and price history; whether its books can be replayed (no token on stdio) |
| `list_live_events()` | live + upcoming events with sources, markets, phrases |
| `list_markets(query, venue?, status)` | public market metadata search |
| `get_market_rules(ticker)` | rules text + phrases for one market |
| `subscribe_phrases(event_id, phrases[], webhook_url?)` | account subscription (needs token) |
| `get_transcript(event_id, since_seq?, limit=2000)` | one page of transcript words + `text` and `last_seq` (pass back as `since_seq`); empty with `redacted_sources` for restricted sources |
| `get_recent_events(event_id?, phrases?, limit)` | latest phrase detections / lifecycle events |
| `get_latency_stats()` | per-source p50/p90 lag vs public stream |
| `watch_event(event_id, phrases?, max_wait_s=30, cursor?)` | long-poll: new phrase hits (with §21 `verdict`) since `cursor`, waits ≤ 60 s; token |

### Langbot Sim

| tool | what |
|---|---|
| `sim_events()` | past events with a feed record and recorded Kalshi books: coverage (book rows, first/last snapshot, tickers, settled markets), Free sample flag, the caller's allowance (public) |
| `sim_run(event_id, strategy, params?, latency_ms?, starting_cash, detail)` | a built-in strategy (`hold_none`, `buy_yes_on_first_counting_hit`, `buy_yes_near_threshold`, `fade_rule_mismatch`) replayed on the feed → scorecard with the latency table and caveats (token; `langbot/per_call_usd` 0.25 via x402 without a plan). Custom code never runs on the server: `langbot sim run --strategy my_bot.py` runs it locally |

### Opt-in strategy tools (`LANGBOT_MCP_STRATEGY_TOOLS=1`)

Off by default: Langbot serves data, not signals. Set `LANGBOT_MCP_STRATEGY_TOOLS=1` in the
client's `env` to list and allow these three; without it they are hidden and cannot be called.

| tool | what |
|---|---|
| `draft_strategy(thesis, speaker?, phrase?, series?, window_days)` | Claude drafts a `langbot.Strategy` file (template without `ANTHROPIC_API_KEY`); with `speaker`/`phrase` it fetches base rates and prices the draft off them |
| `backtest_strategy(code, event_ids[] \| fixture)` | replay through a paper broker → PnL summary |
| `run_paper(strategy_path \| code)` | POSTs to the local daemon `http://127.0.0.1:8787` (demo mode only) |

### Terminal: what's live, what's priced, what's been said, how often before

| tool | what |
|---|---|
| `get_calendar(days=7)` | upcoming events with sources, markets, `speaker_keys` and `rights` |
| `get_market_context(ticker)` | one call per ticker: market + rules, current book, base rate for its speaker/phrase, the event, recent mentions |
| `get_base_rates(speaker?, phrase?, series?, window_days=365)` | how often that mention market settled YES: `yes_rate`, `markets`, `avg_open_price`, `mispricing` = avg(result − open), `by_month` |
| `search_outcomes(speaker?, phrase?, series?, venue?, since?, until?, limit, cursor?)` | the settled markets behind a base rate (venue settlement facts, paginated) |
| `search_mentions(speaker?, phrase?, since?, until?, limit)` | Langbot's own `phrase_detected` facts: phrase, speaker, event, `t_spoken`, `lag_vs_public_ms` — never transcript text |
| `list_speakers(q?)` | canonical speaker keys (`powell`, `jensen-huang`) with aliases and counts |

Each docstring says when an agent should reach for it. Rights (CONTRACTS §10): `get_transcript` returns
full text for `public_domain` / `licensed` sources and phrase facts only for `restricted` / `unknown`
ones; `get_calendar` and `get_market_context` carry `rights` so an agent knows in advance.

Resources: `langbot://events/live`, `langbot://events/{id}/transcript`, `langbot://latency`,
`langbot://calendar`, `langbot://context/{ticker}`, `langbot://markets/{ticker}/brief`, and the
two live (subscribable) ones below.

## Live push

| resource | state |
|---|---|
| `langbot://events/{event_id}/hits` | every `phrase_detected` on the event, each with its §21 `verdict` (null when the feed has none), and a `cursor` |
| `langbot://markets/{ticker}/state` | `said`, `counts` (§21 reading; null = unclear), `mention` / `needed` / `mention_text`, `last_hit`, `verdict` |

How an agent subscribes depends on the MCP revision its client negotiated; the server serves both:

- **2025-11-25 and older**: `resources/subscribe {uri}`; on each `notifications/resources/updated`
  for the URI, `resources/read` it. `resources/unsubscribe` (or disconnecting) stops it.
- **2026-07-28**: `subscriptions/listen` with `resource_subscriptions: [uri, …]`; the same
  notification rides the listen stream (the SDK's `ListenHandler`), closing it unsubscribes.
- **neither**: `watch_event` in a loop with the returned `cursor`.

Upstream is one `WS /v1/stream` per token (stdio: one; hosted HTTP: one per tenant), filtered to
the subscribed events and `types=phrase_detected`, re-filtered with `{"op":"subscribe"}`,
closed when nothing is subscribed, reconnected with jittered backoff (1 → 30 s) and backfilled
from the transcript after a reconnect. See `live.py`. Push over HTTP needs the default stateful
streamable HTTP (SSE responses, the GET stream); with `stateless_http` / `json_response` only
`watch_event` works.

## Paying per call

Some feed endpoints are sold by the call (CONTRACTS §17): replay bundles ($0.50), history
($0.05), evidence packs ($0.25), provenance reports ($0.10), facts ($0.02) — initial list
prices. Tools that map to them carry `_meta` `langbot/per_call_usd` next to the plan fields,
so an agent can read the price before calling, and `get_pricing` passes through the feed's
`per_call` (always) and `x402` (`{enabled: false}` or `{enabled: true, x402_version, scheme,
network, pay_to, asset, asset_decimals, facilitator, max_timeout_seconds, headers, …}`) sections.

The MCP server holds no wallet. When the feed answers an x402 v2-shaped 402, the tool returns a
structured error instead of a bare failure:

```json
{"ok": false, "error": "payment_required", "status": 402, "price_usd": 0.5, "x402Version": 2,
 "accepts": [{"scheme": "exact", "network": "eip155:8453", "amount": "500000", "asset": "0x…USDC", "payTo": "0x…", "maxTimeoutSeconds": 60, "extra": {"name": "USD Coin", "version": "2"}}],
 "pricing_url": "https://langbot.ai/.well-known/langbot-pricing.json", "hint": "…"}
```

The agent can then pay with its own wallet through the SDK (`LangbotClient(x402_wallet=…)` in
`langbot-sdk[x402]`, or `@langbotai/sdk` with `x402Wallet`), or ask the user for a plan token.
A 402 that is not x402-shaped (a plan gate) is reported as before.

## Env

| var | default | |
|---|---|---|
| `LANGBOT_API_URL` | `https://api.langbot.ai` | feed base URL (Dedicated VPC/on-prem deployments set their own) |
| `LANGBOT_API_TOKEN` | — | `lb_…` token, **stdio mode only**; public endpoints work without it |
| `LANGBOT_MCP_STRATEGY_TOOLS` | off | `1` exposes `draft_strategy`, `backtest_strategy`, `run_paper` |
| `ANTHROPIC_API_KEY` | — | enables `draft_strategy` via Claude |
| `LANGBOT_LLM_MODEL` | `claude-opus-5-5` | drafting model |
| `LANGBOT_LLM_EFFORT` | `high` | `output_config.effort` for drafting (`none` to omit) |
| `LANGBOT_DAEMON_URL` | `http://127.0.0.1:8787` | local agent daemon |
| `LANGBOT_BACKTEST_FIXTURE` | — | default JSONL fixture for `backtest_strategy` |
| `LANGBOT_MCP_HOST/PORT/PATH` | `0.0.0.0` / `8001` / `/mcp` | HTTP mode |

## Tokens in HTTP mode

`--http` is multi-tenant: every request must carry `Authorization: Bearer lb_…` and that token is used for the feed call; the server's `LANGBOT_API_TOKEN` is ignored. Tool calls and resource reads without a token are refused with a message pointing at <https://langbot.ai/app/tokens> (listing tools still works). The header is read with `fastmcp.server.dependencies.get_http_headers(include={"authorization"})`; see `get_client()` and `TokenGate` in `server.py`.

## Compliance shape

The server emits **facts** (what was said, when) and runs nothing live. The strategy tools
are opt-in (`LANGBOT_MCP_STRATEGY_TOOLS=1`); when enabled, `run_paper` only
starts demo-mode strategies on the user's local daemon, and `draft_strategy` output is
reviewed by the user before `langbot run`. Live trading requires `--mode live` plus
`LANGBOT_I_UNDERSTAND_LIVE=1` on the user's machine.

## Develop

```bash
uv run --package langbot-mcp pytest services/mcp -q
docker build -f services/mcp/Dockerfile -t langbot-mcp .   # from repo root
```

Registry metadata: `server.json` (MCP registry, `mcp-publisher publish`) and `smithery.yaml`.

## License

MIT (`LICENSE` in this directory). `langbot-mcp` is published on PyPI; it is a thin client over the hosted Langbot feed, which is a proprietary service.
