{
  "markdown": "# gavel-mcp-server\n\nAletheia Analytics MCP server — agent-native interface over the Gavel data product.\n\nThin TypeScript wrapper around `api.thegavel.io`. Exposes Gavel credit data\nand Bitcoin on-chain indicators as MCP tools so LLM-driven agents (Claude\nDesktop, IDE clients, custom agents) can read the data product without\nhand-rolling REST glue.\n\n## Status\n\nAll three layers of the AI concierge spec (`aletheia-docs`\n`data/specs/mcp/ai_concierge.md`) are live, plus the indicator surface and\nreal key→tier resolution. Delivered by **Runbook R18** and governed by the\ndecisions note `data/specs/mcp/tier_and_scope_decisions_v1.md` (MD1–MD12).\n\n**Layer A — read state**\n\n| Tool | Upstream |\n|---|---|\n| `check_wallet_status` | direct RPC (balances, allowances, readiness blockers) |\n| `find_auctions_matching_criteria` | `/v1/auctions` |\n| `get_user_positions` | `/v1/user/:address/positions` |\n| `get_loan_status` | `/v1/loans/:id/status` |\n\n**Layer B — factory model** (unsigned blueprints; the user signs)\n\n| Tool | Encodes |\n|---|---|\n| `prepare_bid_calldata` | `placeBid` + approval when allowance is short |\n| `prepare_create_auction_calldata` | `createAuction` + collateral approval |\n| `prepare_repay_loan_calldata` | `repayLoan` + repayment approval |\n| `prepare_claim_collateral_calldata` | `claimCollateral` |\n| `prepare_claim_refund_calldata` | `claimRefund` |\n\n**Layer C — catalogues**\n\n| Tool | Notes |\n|---|---|\n| `list_wallet_options` | static catalogue, no ranking |\n| `recommend_fiat_onramp` | static catalogue; carries the two-purchase gas requirement |\n\n**Data surface**\n\n| Tool | Upstream |\n|---|---|\n| `list_gavel_indicators` | static catalogue of 32 indicators |\n| `get_gavel_indicator` | `/v1/credit/*`, `/v1/onchain/*`, `/v1/market/*` |\n| `get_yield_curve` | `/v1/yield-curve` |\n| `get_mvrv` | `/v1/onchain/mvrv` |\n| `get_protocol_reference` | static — addresses, signatures, conventions |\n| `list_onchain_indicators` | static catalogue |\n\n### The invariant\n\n**Aletheia builds; the user signs.** There is no signing surface in this\ncodebase — no wallet client, no account, no key material. `viem` is imported\nfor `encodeFunctionData` only. That is what makes \"Aletheia never signs\" an\narchitectural fact rather than a policy promise, and it must stay that way.\n\nEqually load-bearing: **no tool ranks, scores, or selects on the user's\nbehalf.** Filtering by user-supplied criteria is an information service;\nranking by an internal model is investment advice. `find_auctions_matching_criteria`\nis named as it is deliberately, and the naming is not cosmetic.\n\n## Architecture\n\n```\nLLM Client → mcp.thegavel.io (this server) → api.thegavel.io (REST) → PostgreSQL\n              [tool catalog, descriptions,        [authoritative endpoints]\n               response shaping, auth, limits]\n```\n\nSingle source of truth: the REST API. The MCP server **never** queries\nPostgres directly. Tools shape responses for LLM consumption (JSON-stringified\ntext content) but never re-implement business logic. When the REST API\nupgrades, MCP inherits the upgrade automatically.\n\n## Local development\n\n```bash\n# Install deps (Node 20+)\nnpm install\n\n# Copy and edit env file\ncp .env.example .env\nnano .env  # set GAVEL_API_BASE_URL etc.\n\n# Dev mode (tsx watch)\nnpm run dev\n\n# Type check\nnpm run typecheck\n\n# Build to dist/\nnpm run build\n```\n\nPoint a development MCP client (MCP Inspector, Claude Desktop with an HTTP\nconnector) at `http://localhost:3002/mcp` to exercise tools.\n\n## Deployment\n\nTarget: `gavel-btc` Hetzner host, alongside `gavel-api`.\n\n```bash\n# Local — build and stage\nnpm install\nnpm run build\n\n# Copy to server\nscp -r dist/ package.json package-lock.json deployment/ \\\n    root@gavel-btc:/root/gavel-mcp/\n\n# On server — install runtime deps (not the full dev set)\nssh root@gavel-btc\ncd /root/gavel-mcp\nnpm install --omit=dev\n\n# Configure\ncp .env.example .env\nnano .env\n# Set:\n#   GAVEL_API_BASE_URL=https://api.thegavel.io  (public API, for tool reads)\n#   GAVEL_API_INTERNAL_URL=http://127.0.0.1:4012  (loopback, for tier lookup)\n#   INTERNAL_API_SECRET=<must match gavel-indexer/.env.mainnet>\n#   PORT=3002\n#   NODE_ENV=production\n\n# Install systemd unit\ncp deployment/gavel-mcp.service /etc/systemd/system/\nsystemctl daemon-reload\nsystemctl enable gavel-mcp.service\nsystemctl start gavel-mcp.service\n\n# Verify\njournalctl -u gavel-mcp -n 50 --no-pager\ncurl http://localhost:3002/health\n\n# Reverse proxy\ncp deployment/nginx-mcp.conf /etc/nginx/sites-available/mcp.thegavel.io\nln -s /etc/nginx/sites-available/mcp.thegavel.io \\\n      /etc/nginx/sites-enabled/mcp.thegavel.io\nnginx -t && systemctl reload nginx\n\n# TLS (Let's Encrypt)\ncertbot --nginx -d mcp.thegavel.io\n\n# End-to-end check\ncurl https://mcp.thegavel.io/health\n```\n\n## Configuration\n\nAll knobs live in `.env`:\n\n| Var | Default | Purpose |\n|---|---|---|\n| `PORT` | `3002` | HTTP listen port |\n| `NODE_ENV` | — | `production` for JSON logs |\n| `LOG_LEVEL` | `info` | pino level (`trace`/`debug`/`info`/`warn`/`error`) |\n| `GAVEL_API_BASE_URL` | `http://localhost:3001` | Upstream REST base URL |\n| `RATE_LIMIT_ANONYMOUS_PER_MINUTE` | `60` | Anonymous bucket size |\n| `RATE_LIMIT_PAID_PER_MINUTE` | `300` | Paid bucket size |\n| `CORS_ALLOWED_ORIGINS` | empty | Comma-separated; empty = no CORS |\n| `HEALTH_CHECK_SECRET` | empty | If set, `/health` requires `?secret=...` |\n| `GAVEL_API_INTERNAL_URL` | `http://127.0.0.1:4012` | Key→tier lookup. Must be the **loopback** address — `/internal/resolve-tier` refuses any request carrying an `X-Forwarded-For`, so the public `api.thegavel.io` host will not work |\n| `INTERNAL_API_SECRET` | empty | Shared secret for the tier lookup. Must match `gavel-indexer/.env.mainnet`. Unset ⇒ every caller resolves to `free` |\n| `MCP_TIER_ENFORCEMENT` | `false` | Enforce per-tool tiers. Leave `false` until Gate B — see *Tier model* |\n| `ARBITRUM_RPC_URL` | public RPC | Chain reads for Layer A/B. Point at a paid endpoint in production |\n| `ARBITRUM_SEPOLIA_RPC_URL` | public RPC | Testnet equivalent |\n\n## Tier model\n\nThe ladder is `free / pro / enterprise` — **identical** to the product's\n(`gavel-indexer/lib/tiers.js`) and to what Stripe sells. The scaffold's\noriginal `anonymous / developer / professional / enterprise` was a second\nvocabulary for one entitlement and is retired (MD1).\n\n`gavel-indexer/lib/api-keys.js` is **the** authority on what tier a key is.\nThe MCP does not open its own database pool; it asks\n`GET /internal/resolve-tier` over loopback, caches the answer for 60 s, and\n**fails open to `free`** on any error. A data MCP that 500s because the key\ndatabase hiccuped is worse than one that briefly serves anonymous.\n\n### Enforcement is authored but OFF\n\n`MCP_TIER_ENFORCEMENT` defaults to `false`, and that is the correct state\ntoday. Monetisation is gated until Gate B (D16–D18): *do not build a paywall\nuntil someone has asked to pay*. Runbook A2 withdrew the commercial surface,\nand `www.thegavel.io/pricing` currently states that data access is free and\nopen — so refusing a tool and pointing the user at a page which denies tiers\nexist would be a self-refuting journey.\n\nWith the flag off, `requireTier` still resolves the caller's real tier and\n**logs what it would have refused**. That log is the evidence for M6, the\n\"has anyone actually asked to pay?\" gate condition.\n\n**Before flipping it on**, read MD2. There are two incompatible readings of\nwhat a paid MCP means — whole-surface-paid (`lib/tiers.js` carries\n`mcp: false` on free) versus depth-paid (MD3, the endorsed one). They are\nvery different products.\n\n### What is free, and why\n\nPer MD3, inheriting the D5 route/depth map: raw on-chain state, auction\ndiscovery, wallet status, commodity on-chain indicators, the **current**\nvalue of any Gavel-derived assessment, and **history** are all free. History\nis free because D9 retired the 30-day REST cap, and the MCP must not\nreintroduce a fence the surface it mirrors has abandoned. The paid boundary\nis **bulk delivery**, which this server does not offer.\n\nParticipation is never gated (D3). Every Layer A/B/C tool is `free`: a\nwould-be bidder must never meet a paywall between deciding to bid and being\nable to.\n\nRate limits are infrastructure protection, not a billing meter (D2), and\napply irrespective of the enforcement flag.\n\n## Redeploying a change\n\n```bash\nnpm run build            # tsc -> dist/ ; must be clean\nsystemctl restart gavel-mcp\nsystemctl is-active gavel-mcp\njournalctl -u gavel-mcp -n 30 --no-pager\n```\n\n**This service is systemd, not pm2.** pm2 on this host carries\n`quorum-mcp-testnet`, a different service — `pm2 restart gavel-mcp` is a\nno-op that looks like a successful deploy. R18 v1 had this wrong; it is\nrecorded in that runbook's §8.\n\nThe service runs `dist/`, not `src/`, so a change that is not built is a\nchange that is not deployed.\n\n## Adding a tool\n\n1. Create `src/tools/<category>/<name>.ts`. Copy `credit/yield-curve.ts` as\n   the template — it's the cleanest worked example.\n2. Define a Zod schema for inputs with `.describe()` on every field; that\n   description is what the LLM sees during tool discovery.\n3. Write the tool description as a multi-line string. Lead with what the\n   indicator is, give interpretive context (without recommending anything),\n   and document the response shape. The MCP SDK uses this verbatim in the\n   catalog.\n4. Body: `requireTier(...)` → `upstreamGet(...)` → return\n   `{ content: [{ type: 'text', text: JSON.stringify(...) }] }`.\n5. Register the tool in `src/tools/index.ts`.\n6. Add an entry to `src/tools/discovery/list-onchain.ts` (or the equivalent\n   discovery catalog for that domain).\n\n## Testing manually\n\n```bash\n# 1. Health\ncurl -s http://localhost:3002/health | jq\n\n# 2. MCP Inspector\nnpx @modelcontextprotocol/inspector\n# Connect to http://localhost:3002/mcp\n# Verify: tools/list returns 3 tools, get_yield_curve returns live data,\n# get_mvrv returns a structured McpError \"not found\".\n```\n\n## License\n\nProprietary © 2026 Aletheia Analytics SASU. All rights reserved.\n",
  "bytes": 10013,
  "sha": "7e71b722f5efeb4298cf6ad8168d813deea4d524cd0f91d800a76d50a18638b8",
  "repo_slug": "jamieframe/gavel-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_bitcoincreditstack_mcp_e345c8b0/readme"
}