Hosted Model Context Protocol (MCP) server for Powerloom BDS: catalog-derived HTTP tools (same idea as bds-agent mcp on stdio), plus verify_data_provenance and get_credit_balance, exposed over SSE/HTTP so remote clients (OpenClaw, LangGraph, Claude Desktop with remote MCP, Cursor, etc.) can connect without running a local subprocess.
- Local stdio vs this server
- Prerequisites
- Install
- Configuration
- Run
- Usage: HTTP transport and auth
- Tools
- Connect from MCP clients
- Docker
- Operations and troubleshooting
- Development
bds-agent mcp (bds-agent-py) |
bds-mcp-server (this repo) |
|
|---|---|---|
| Transport | stdio (child process) | HTTP SSE + POST messages |
| Deploy | None; client starts bds-agent mcp |
Run as a service (VM, K8s, Docker) |
| Auth | Profile / env API key in the child env | Authorization: Bearer on every HTTP request |
| Catalog | BDS_API_ENDPOINTS_CATALOG_JSON / BDS_SOURCES_JSON |
BDS_MCP_CATALOG_PATH or BDS_MCP_CATALOG_URL |
Use stdio for Cursor/Claude on your laptop when the CLI can launch the process. Use this server when the agent runs elsewhere or you want a stable URL and shared deployment.
- Python 3.12+
- Core API reachable at
BDS_MCP_BASE_URLwith/mpp/...routes (same origin the catalog describes). - Metering at
BDS_MCP_METERING_URLexposingGET /credits/balancewith Bearer auth (used to validate API keys before MCP traffic). - An
endpoints.jsoncatalog (e.g. from snapshotter-computesapi/endpoints.json).
With uv:
uv sync --extra devRun commands via uv run … (see Run). To put bds-mcp-server on your PATH:
uv tool install .After git pull or code changes, if uv tool install --force . still runs old code, run uv cache clean first, then uv tool install --force . again (uv may reuse wheels when the version string is unchanged). Editable: uv tool install --force --editable ..
With pip only:
pip install ".[dev]"All settings use the BDS_MCP_ prefix. Copy .env.example to .env and load it in your process manager or docker run --env-file.
| Variable | Required | Default | Purpose |
|---|---|---|---|
BDS_MCP_BASE_URL |
yes | — | Upstream Core API origin (same role as BDS_BASE_URL for bds-agent): catalog tools call GET here. Not the URL of this MCP server. |
BDS_MCP_METERING_URL |
yes | — | Metering origin; server calls GET {origin}/credits/balance |
BDS_MCP_CATALOG_PATH or BDS_MCP_CATALOG_URL |
yes (one of) | — | Local filesystem path to endpoints.json, or HTTP URL to fetch at startup |
BDS_MCP_HOST |
no | 0.0.0.0 |
Bind address for this MCP server |
BDS_MCP_PORT |
no | 8808 |
Listen port (/sse, /messages/, /health) |
BDS_MCP_CATALOG_PATH_PREFIXES |
no | /mpp |
Comma-separated path prefixes to filter catalog routes; use all to disable filtering |
BDS_MCP_POWERLOOM_RPC_URL |
no | — | JSON-RPC URL for verify_data_provenance (eth_call) |
BDS_MCP_PROTOCOL_STATE_ADDRESS |
no | mainnet BDS default | ProtocolState contract |
BDS_MCP_DATA_MARKET_ADDRESS |
no | mainnet BDS default | DataMarket contract |
BDS_MCP_AUTH_CACHE_TTL_SECONDS |
no | 60 |
Cache successful metering checks (seconds) |
BDS_MCP_INTERNAL_BILLING_SECRET |
no | — | Reserved for future internal billing; not used in Phase 1 |
Clients reach this MCP server at http(s)://<your-host>:<port> (or your reverse proxy). That host is not BDS_MCP_BASE_URL (upstream Core API).
Reference catalog: BDS Mainnet Uniswap V3 endpoints.json in snapshotter-computes — mostly /mpp/... GET routes; a single "sse": true entry (/mpp/stream/allTrades) for the live trade stream. Use that URL as BDS_MCP_CATALOG_URL or copy the file locally for BDS_MCP_CATALOG_PATH.
Catalog filter: Only routes whose path equals or starts with a listed prefix (after normalization) become MCP tools. If filtering removes every route, the server refuses to start.
Set env vars (or use .env with your process manager), then start the server.
With uv (after uv sync --extra dev):
export BDS_MCP_BASE_URL=https://your-core-api.example
export BDS_MCP_METERING_URL=https://your-metering.example
export BDS_MCP_CATALOG_PATH=/path/to/endpoints.json
uv run bds-mcp-server
# or: uv run python -m bds_mcp_server.serverAfter uv tool install .:
export BDS_MCP_BASE_URL=... BDS_MCP_METERING_URL=... BDS_MCP_CATALOG_PATH=...
bds-mcp-serverWith pip / plain Python:
python -m bds_mcp_server.serverThe process loads the catalog once at startup, then serves MCP until stopped.
This server uses the MCP Python SDK SSE transport (see mcp.server.sse.SseServerTransport).
-
Health (no auth)
GET /health→ JSON such as{"status":"ok","service":"bds-mcp-server"}. -
SSE session
GET /ssewith headerAuthorization: Bearer <your_powerloom_api_key>.
The server validates the key against metering (GET /credits/balance) before opening the stream. -
Client messages
The SSE stream sends an initialendpointevent whosedatais the path + query (e.g./messages/?session_id=...) where the client must POST JSON-RPC MCP messages. Every POST to that URL must also send the sameAuthorization: Bearerheader; the server validates again (with TTL cache). -
Upstream BDS calls
For catalog tools, the MCP server forwards the same Bearer token toBDS_MCP_BASE_URL. Credits are deducted by the Core API middleware (Phase 1: no separate deduct in this service).
Invalid or exhausted credits: Metering returns 401 or 402, or balance ≤ 0 → the server rejects the HTTP request with an error body; MCP methods are not executed.
One MCP tool per filtered endpoints.json route, same naming as local MCP: names typically start with bds_, derived from path and method. Most routes are GET snapshots against the Core API; only entries with "sse": true in the catalog use SSE upstream (in the reference file that is /mpp/stream/allTrades only)—those MCP tools collect up to max_events (default 5, max 50) and return events plus last known credit header.
Parameters: cid, epoch_id, project_id, optional data_market.
Requires BDS_MCP_POWERLOOM_RPC_URL and correct ProtocolState/DataMarket config. If RPC is not set, the tool returns a clear configuration error.
No parameters. Calls metering GET /credits/balance with the client’s Bearer token and returns the JSON body (e.g. balance and rate limit fields), depending on your metering API.
bds-agent is not this client. The CLI’s bds-agent mcp command runs a stdio MCP server and talks to the Core API itself. bds-mcp-server is a separate process that listens on HTTP; any MCP client that supports SSE (or your wrapper) connects to /sse and sends the same Bearer token your agent would use against the Core API.
What you need: the Powerloom API key (the same one you use for bds-agent / Authorization: Bearer against the snapshotter). Put it in the MCP client’s config as Authorization: Bearer <key> on both the SSE GET and the message POSTs.
Remote MCP is supported; use --transport sse and --header for the Bearer token (options must come before the server name). Example for a server on localhost:
claude mcp add --transport sse \
--header "Authorization: Bearer YOUR_POWERLOOM_API_KEY" \
bds-mcp-local \
http://127.0.0.1:8808/sseSee Anthropic’s Claude Code MCP docs for --scope, JSON config, and headersHelper if you prefer not to paste the key on the command line. Their docs also show streamable HTTP transport; this server only implements SSE today.
If the product supports remote MCP over SSE, add a server entry for https://<host>:<port>/sse and set the Authorization header to Bearer <key> (exact UI varies by version).
Use their MCP adapter with the SSE URL and the same Bearer on all requests to this server.
scripts/list_mcp_tools.py connects to this server’s /sse using the MCP client’s HTTP SSE transport (how Claude/Cursor talk to this process). It lists all MCP tools—including mostly GET catalog tools and any BDS stream tool your catalog defines—not “SSE-only” upstream routes.
cd /path/to/bds-mcp-server
uv sync --extra dev
export BDS_MCP_SSE_API_KEY=your_key
uv run python scripts/list_mcp_tools.pyYou should see one line per tool name (catalog tools + verify_data_provenance + get_credit_balance). curl http://127.0.0.1:8808/health checks the process only; GET /sse needs a valid Bearer and is easier to verify with this script than raw curl.
The Dockerfile uses pip against pyproject.toml (standard slim image). uv.lock is for local uv sync / CI; if you need the image to match the lockfile exactly, add a build step such as uv pip sync from an exported requirements file or use a uv base image—same idea as other Python services.
docker build -t bds-mcp-server .
docker run --env-file .env -p 8808:8808 bds-mcp-serverEnsure .env sets BDS_MCP_BASE_URL, BDS_MCP_METERING_URL, and BDS_MCP_CATALOG_PATH or BDS_MCP_CATALOG_URL. For production, put TLS on a reverse proxy in front of the container; do not expose metering or core secrets in the image.
| Symptom | What to check |
|---|---|
| Process exits at startup | Catalog load failed (bad path/URL), or no endpoints after path-prefix filter — fix env and catalog |
401 on /sse or /messages/ |
Missing/wrong Authorization: Bearer, or metering rejects the key |
502 (or older builds: 500) on /sse / POST during auth |
Metering GET /credits/balance failed (timeout, TLS, or Name or service not known). BDS_MCP_METERING_URL must be a hostname the MCP container/VM can resolve (public URL or correct cluster DNS)—not a name that only resolves on a developer machine. Current server code returns 502 with a JSON error body instead of an unhandled 500. |
| 402 or “balance zero” | Add credits in metering; key is valid but not billable |
verify_data_provenance errors |
Set BDS_MCP_POWERLOOM_RPC_URL; verify ProtocolState/DataMarket addresses match the chain |
| Catalog tools return HTTP errors from core | BDS_MCP_BASE_URL wrong, or key lacks access; check Core API logs |
uv sync --extra dev
export BDS_MCP_BASE_URL=http://x BDS_MCP_METERING_URL=http://m \
BDS_MCP_CATALOG_PATH=tests/fixtures/endpoints.minimal.json
uv run pytest tests/ -v
uv run ruff check src testsEquivalent with pip: pip install -e ".[dev]" then pytest / ruff without uv run.
Deploy as its own git repository; license file at repository root when published.