feat/batu-mcp
needs attentionviewing older commitee41aea · fullpre-PRreviewed 2026-07-09 19:12 UTC5H · 6M · 11L · 5I- Purpose
- Ship an official Batu Energy MCP server as an npm-publishable package so AI agents can work with Mexican CFE energy data without custom integration code.
- Goal
- Publish @batu/mcp-server with 12 tools covering the full public API surface (bills, files, jobs, monitoring, webhooks) and register it as the repo's dev-mode batu MCP server.
- Sub-goals
- Implement BatuClient with JWT lifecycle (exchange bk_... key → Bearer, cache, refresh 60s before expiry, single 401 retry)
- Register 12 MCP tools with accurate Zod schemas and agent-oriented descriptions
- Wire into .mcp.json as dev-mode entry (tsx source path)
- Add unit tests for auth lifecycle at the trust boundary
- Fix date-filter param names + add client tests + publish config
- What
- New package packages/mcp-server with 7 files (+756 lines, all additions). Added batu entry to .mcp.json. No existing code modified.
- Why
- AI agents (Claude Code, Claude Desktop, third-party integrations) need a supported, typed, npm-installable way to query CFE bills and energy data — the public API is the canonical source and this MCP server exposes it as tool calls.
- Areas
- packages/mcp-server+745−0.mcp.json+11−0
- Blast
- New package only — 7 files, +756 lines (all adds). No changes to existing domain code, APIs, DB schema, or infra.
Findings · 26
correctness7
mintToken race condition: concurrent requests both trigger a token mint
packages/mcp-server/src/client.ts:76
getToken() checks tokenState then calls mintToken() without mutual exclusion. Fix: cache the in-flight mint Promise.
batu_download_files_zip: no client-side filter guard — empty body {} POSTs org-wide bundle
packages/mcp-server/src/index.ts:182
All 3 filter fields are optional with no .refine(). Add .refine(a => a.rpu || a.bill_id || a.type, { message: 'At least one filter required' }).
expiresAtMs negative if expires_in ≤ 60 → infinite re-mint loop
packages/mcp-server/src/client.ts:71
Guard: Math.max(0, expiresIn - TOKEN_REFRESH_MARGIN_S)
401 retry path: same race as cold-start mint (both resolved by mint-Promise cache)
packages/mcp-server/src/client.ts:111
Covered by the same fix as the cold-start race.
clientSingleton lazy init hides missing BATU_API_KEY at startup
packages/mcp-server/src/index.ts:37
.mcp.json uses tsx source path — breaks outside the monorepo
.mcp.json:38
latest_only boolean serialized as string 'true'/'false' — confirm API contract
packages/mcp-server/src/index.ts:160
security5
batu_create_webhook: HTTP URLs accepted — HTTPS not enforced in Zod schema
packages/mcp-server/src/index.ts:307
Add .refine(u => u.startsWith('https://'), 'Webhook URL must use HTTPS').
BATU_API_URL not validated — could redirect client to attacker-controlled server
packages/mcp-server/src/client.ts:48
period_count: -1 allows unlimited credit burn in autonomous agent loops
packages/mcp-server/src/index.ts:215
Webhook signing secret in tool response — no redaction guidance for MCP hosts with logging
packages/mcp-server/src/index.ts:289
API key not logged or included in error messages — confirmed safe
packages/mcp-server/src/client.ts:18
conventions5
Missing lint script — package silently excluded from Turbo CI lint pipeline
packages/mcp-server/package.json:34
Add: "lint": "eslint . --max-warnings 0" and eslint.config.js extending @batu/eslint-config/base.
Missing vitest.config.ts — test environment not declared
packages/mcp-server/package.json:39
Add vitest.config.ts: { test: { globals: true, environment: 'node' } }
UNLICENSED on a publicly-published npm package blocks third-party use
packages/mcp-server/package.json:7
Choose MIT/Apache-2.0 for open use, or private:true for closed-source.
Section banner comments in index.ts violate no-what-comments convention
packages/mcp-server/src/index.ts:74
Remove // === Bills === etc. banners.
moduleResolution: Bundler with plain tsc may mis-resolve .js extensions at runtime
packages/mcp-server/tsconfig.json:2
tests6
mintToken: empty access_token string caches blank Bearer token instead of throwing
packages/mcp-server/src/client.ts:61
!body?.data?.access_token check passes for empty string. Add test for 200 with access_token:''.
batu_list_files: latest_only / bill_id mutual exclusion not enforced or tested
packages/mcp-server/src/index.ts:143
Add Zod .refine(a => !(a.latest_only && a.bill_id)) and matching test.
Concurrent mintToken calls not tested — race undetectable by CI even after a fix
packages/mcp-server/src/client.ts:76
run() errorResult branches not directly tested in tool layer
packages/mcp-server/src/index.ts:51
MCP server is an I/O adapter — BatuClient as trust boundary is the correct test target
packages/mcp-server/src/client.test.ts
Existing BatuClient tests are thorough and well-structured
packages/mcp-server/src/client.test.ts
improvement3
batu_list_files: limit/cursor lack .describe() unlike other paginated tools
packages/mcp-server/src/index.ts:154
Add .describe() to limit (max differs: 100 here vs 5000 for bills) and cursor.
README missing guidance on enumerating all org RPUs
packages/mcp-server/README.md:39
mintToken/getToken two-method split is correct — no simplification needed
packages/mcp-server/src/client.ts:51