← all branches

feat/batu-mcp

needs attentionviewing older commit
844f42f · fullPR #199reviewed 2026-07-09 20:13 UTC0H · 3M · 5L · 3I
The branch
Purpose
Create an agent-facing projection of the Batu Energy public API v1 as an MCP server, so AI agents can work with Mexican power-sector data (CFE bills, jobs, monitoring, webhooks) without custom integration code.
Goal
Ship @batu/mcp-server as a standalone npm package — one-line install in Claude Desktop, Claude Code, or Cursor.
Sub-goals
  • SG-1: BatuClient — API key → JWT lifecycle (exchange, cache, 401 retry)
  • SG-2: 12 MCP tools over stdio covering the full agent loop (bills, files, jobs, monitoring, webhooks)
  • SG-3: Zero workspace runtime deps (only @modelcontextprotocol/sdk + zod) — publishable standalone
  • SG-4: mcp-server.md drift-governance rule to prevent future param-name drift
  • SG-5: .mcp.json dev entry for in-repo use
The changes (whole branch)
What
New package packages/mcp-server with BatuClient (client.ts), 12 tool definitions (index.ts), Vitest tests (client.test.ts), tsconfig, package.json, README. New .claude/rules/mcp-server.md governance rule. .mcp.json updated to add batu server entry. Deleted .branch/scope.md and .branch/intent.md (leftover from previous branch work).
Why
Batu's public API is the best way for AI agents to integrate with CFE data; an MCP server makes it zero-friction. The governance rule was added to prevent the silent param-drift bug that was caught in the #199 audit (period_start vs period_start_from).
Areas
packages/mcp-server+7800.claude/rules/mcp-server.md+810.mcp.json+110pnpm-lock.yaml+54534
Blast
4 areas, ~1417 lines net new. No changes to existing code — new package + dev config + governance rule only.
new-npm-package publishable-standalone auth-lifecycle-change mcp-tools
typecheck· node_modules not installed on this runner — could not run tsctests· node_modules not installed on this runner — could not run vitestcoderabbit· No .coderabbit.yaml present

Findings · 11

correctness3

medium

Token expiry math: expiresIn < 60 causes constant re-minting

packages/mcp-server/src/client.ts:71

expiresAtMs = Date.now() + (expiresIn - TOKEN_REFRESH_MARGIN_S) * 1000. If the server ever returns expires_in < 60 (degraded auth, short-lived token), expiresAtMs is in the past and getToken() always sees it as expired, triggering a mintToken() call on every single request. Fix: clamp the margin to Math.max(expiresIn - TOKEN_REFRESH_MARGIN_S, 0) * 1000, or at minimum log a warning when expiresIn is unexpectedly small.

low

batu_list_bills missing closed-vocab `source` filter

packages/mcp-server/src/index.ts:78

The validator (parseBillsListQuery:282) accepts source as a validated enum CSV: manual|api|email|ocr|xml|inferred|payment_check. The tool omits it and the description doesn't mention it. Unlike free-text filters that silently yield empty results when unmatched, source is enum-validated — a future agent that discovers this field from the API docs and sends it will have it silently dropped by .passthrough(), not rejected. The mcp-server rule says closed-vocab enums (like status, payment_status) must be mirrored; source is in that category.

low

batu_collect_bills missing `site_name` — new RPUs onboarded without location label

packages/mcp-server/src/index.ts:232

parseJobCreateBody (line 403) accepts optional site_name for tagging the physical location when registering a new RPU. Not exposed in the tool. An agent managing a multi-site org that bulk-onboards RPUs via MCP creates unlabelled sites, requiring manual cleanup in the platform UI. service_name (CFE label) is exposed but site_name (customer location label) is not. One-liner fix: add site_name: z.string().optional().describe('Location label for this site (optional, applies when RPU is new to your org)') and spread it into the body.

security4

medium

Non-BatuApiError exceptions expose raw Node.js error messages to the MCP client

packages/mcp-server/src/index.ts:55

errorResult() falls back to `e instanceof Error ? e.message : String(e)`. Node.js network errors (ENOENT, ECONNREFUSED, DNS failures, TLS errors) routinely embed system paths, hostnames, and socket details. These surface to the AI model's context and can appear in chat output visible to end users. Additionally, a malformed BATU_API_URL would produce a URL-construction error containing the env var value (including any accidentally embedded credentials). Recommend a generic fallback: 'Network error. Check connectivity and BATU_API_URL.' for non-BatuApiError exceptions.

low

BATU_API_URL override is an undocumented trust boundary — API key sent to configured URL

packages/mcp-server/src/client.ts:48

mintToken() POSTs the raw API key to {baseUrl}/auth/token. If BATU_API_URL is attacker-influenced (compromised .mcp.json, .env file on a shared machine), the key is exfiltrated. This is a documented-but-low risk for a user-controlled stdio process, but the variable is not called out as a trust boundary in the README or constructor error message. Worth documenting: BATU_API_URL should only be set to a trusted server.

low

npx -y install command has no integrity pin in README

packages/mcp-server/README.md

The recommended install command npx -y @batu/mcp-server auto-installs without a version pin. A compromised npm account publishing a malicious @batu/mcp-server version would be auto-pulled by users who restart their MCP host. Recommend pinning to a version in the README: npx -y @batu/mcp-server@0.1.0 until a stable release cadence and Provenance attestation are in place.

info

Token mint race: concurrent request() calls each trigger mintToken()

packages/mcp-server/src/client.ts:76

getToken() is not serialized — concurrent callers that all arrive before the first token is cached each mint a fresh JWT, sending apiKey to /auth/token multiple times. In an MCP stdio server, request() calls are serialized by the protocol, so this is theoretical. If BatuClient is ever used in a concurrent context, fix with a mintingPromise: Promise<string> | null guard field.

conventions1

medium

Missing tools/list smoke test — mandatory per mcp-server.md step 5

packages/mcp-server/src/client.test.ts

mcp-server.md step 5 (added in this same PR) reads: 'new param wiring is best guarded by a tools/list smoke assertion.' No such test exists. This is load-bearing: the public-v1 contracts use z.unknown().passthrough() so a wrong param name silently drops the filter and returns unfiltered data — exactly the bug the rule was written to prevent (prior audit: period_start vs period_start_from). The smoke test is lightweight: build the server, pipe the two JSON-RPC initialization + tools/list messages via stdio, assert all 12 tools are present and spot-check that batu_list_bills has period_start_from.

tests1

info

errorResult non-BatuApiError branch untested

packages/mcp-server/src/index.ts:51

The fallback branch (plain Error / unknown) in errorResult() is never exercised by client.test.ts. Not critical since the branch is two lines, but the project convention is to test both error paths. A tools/list smoke test or a dedicated index.ts unit test could cover this.

improvement2

low

batu_list_files `limit` param missing .describe() — agents see no default or ceiling

packages/mcp-server/src/index.ts:188

limit: z.number().int().min(1).max(100).optional() has no .describe() call. The analogous batu_list_bills limit has .describe('Page size (default 25, max 5000)'). Agents reading the batu_list_files schema won't know the default or that the ceiling is 100 (different from bills' 5000). Fix: add .describe('Page size (default 25, max 100)').

info

.mcp.json dev entry silently fails if mcp-server node_modules not installed

.mcp.json:51

The dev batu entry uses npx -y tsx packages/mcp-server/src/index.ts. If node_modules aren't installed for the mcp-server package, Claude Code shows the batu tool as unavailable with no actionable error. Consider pointing at the built dist after a one-time pnpm --filter @batu/mcp-server build, or add a comment in the surrounding README noting the prerequisite.

History · 5 commits

  1. 59a73bdneeds attentionincremental1H · 6M · 5L2026-07-10 05:23
  2. 5ca7218needs attentionincremental2H · 5M · 5L2026-07-10 04:56
  3. 0e187bdneeds attentionincremental0H · 1M · 2L2026-07-10 04:39
  4. 844f42fneeds attentionfull0H · 3M · 5L2026-07-09 20:13current
  5. ee41aeaneeds attentionfull5H · 6M · 11L2026-07-09 19:12