← all branches

feat/batu-mcp

needs attention
59a73bd · incrementalpre-PRreviewed 2026-07-10 05:23 UTC1H · 6M · 5L · 5I
The branch
Purpose
Ship an official Batu Energy MCP server so AI agents (Claude, Cursor, any MCP client) can work with CFE bills, payment status, files, collection jobs, monitoring, webhooks, energy metrics, savings reports, and asset management — without custom integration code.
Goal
Production-ready @batu/mcp-server package: 33 tools over the public-v1 API, correct auth lifecycle (concurrent-mint dedup, TTL floor), error redaction, test suite, governance rule (mcp-server.md), and user-facing docs.
Sub-goals
  • SG-1: Core CFE tools — bill list/get, payment status, files, collection job, monitoring, webhooks (12 tools, earlier commits)
  • SG-2: Energy / savings / identity tools — batu_whoami, batu_get_energy_summary, savings CRUD (6 tools)
  • SG-3: Asset-management reads + batch provision — sites, assets, streams, sources, integrations, catalogs, batu_provision_assets (15 tools)
  • SG-4: Token lifecycle correctness — concurrent-mint dedup (mintingPromise ??=) + MIN_TOKEN_TTL_S floor
  • SG-5: Test suite — unit (client, results), tools/list smoke, live-smoke script
  • SG-6: Drift governance — .claude/rules/mcp-server.md + sync-mcp-tools skill
  • SG-7: User docs — README expansion, /developers MCP section, i18n install strings
The changes (whole branch)
What
Incremental window (ee41aea..59a73bd): extracted run/jsonResult/errorResult helpers to results.ts for unit-testability; added MIN_TOKEN_TTL_S floor + mintingPromise dedup to client.ts; added 21 new tools (identity, energy summary, savings CRUD, full asset-management read surface, explore, provision); expanded README with new tools table and testing section; added tools-list smoke test; added trust boundary note; added mcp-server.md governance rule and sync-mcp-tools skill.
Why
Agent-facing API coverage was incomplete (no energy/savings/asset-management exposure). Token lifecycle had an edge case (sub-60s expires_in caused hot re-mint). Results helpers were inline in index.ts making them untestable. No governance rule meant MCP/API drift was invisible.
Areas
packages/mcp-server+100050.claude/rules+990.claude/skills/sync-mcp-tools+370apps/platform/src+260pnpm-lock.yaml+54534
Blast
Additive only — packages/mcp-server is a new package with no callers in the platform. The /developers page change adds content but removes nothing. No production request paths modified.
private:true — package not yet published; install docs are aspirational
typecheck· Not run — no CI data available for this branch (no open PR; check-runs API returned 403)tests· No CI data available; local test suite not run in this reviewcoderabbit· No .coderabbit.yaml in repo

Findings · 16

correctness2

high

private:true blocks the npx install that README and dashboard UI strings advertise

packages/mcp-server/package.json:4

package.json sets "private": true (intentional staging) but README and en.json/es.json mcpInstall strings both reference `npx -y @batu/mcp-server@0.1.0`. npm refuses to publish a private package, so any user following those instructions today gets a 404. Either gate/remove the install docs until the package is published, or add a clear 'not yet published' notice. The README 'Manual, from any MCP client' section already shows the local-build path — consider promoting that as the current install method.

info

Concurrent-mint dedup is sound — mintingPromise ??= pattern is correct

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

Both parallel callers share the same mintToken() Promise. On success, finally() clears mintingPromise after tokenState is set, so subsequent calls take the fast path. On failure, both callers get the rejection; next call retries cleanly. The 401-retry path (tokenState = null + retried:true) is also safe: both retried callers share a fresh mint via mintingPromise ??= again.

security4

medium

No HTTPS enforcement on BATU_API_URL — API key can be sent in cleartext

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

The constructor accepts any URL scheme from BATU_API_URL without validation. An accidental http:// staging URL sends the API key in cleartext. The README trust-boundary note is documentation-only. Fix: validate `protocol === 'https:'` in the constructor (with a localhost carve-out if needed).

medium

Dashboard mcpInstall strings omit version pin; contradict README guidance

apps/platform/src/messages/en.json:3093

en.json / es.json render `npx -y @batu/mcp-server` (floating latest); README pins `@0.1.0` and explicitly explains why: 'a restart can't silently pull a new (or compromised) publish.' The UI strings should match. When the package is eventually published, users who configured from the dashboard get an unpinned install.

low

publishConfig.access:public latent conflict with private:true

packages/mcp-server/package.json:31

private:true blocks publishing today, but publishConfig.access:"public" remains and would silently make the scoped @batu package public on npm if private is ever lifted. Either remove publishConfig until ready to publish, or add a comment documenting the intent.

info

batu_create_webhook MCP tool accepts http:// webhook URLs (Zod .url() allows it)

packages/mcp-server/src/index.ts

The tool description says 'HTTPS endpoint URL' but z.string().url() allows http://. The server handler also doesn't enforce HTTPS. A .refine(u => u.startsWith('https://'), 'URL must use HTTPS') on the tool param would match the stated constraint. (Server-side enforcement is the primary guard.)

conventions1

medium

batu_list_metric_streams: search and status fields missing .describe()

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

The mcp-server.md rule says '.describe() each field'. batu_list_metric_streams exposes `search: z.string().optional()` and `status: amStatus.optional()` with no description text. The shared `amStatus` definition also has no .describe(), so all 6 tools using it bare expose an undescribed enum to agents. Add descriptions to the shared definition or at the callsite.

tests6

medium

No test for MIN_TOKEN_TTL_S floor — silent hot-remint regression risk

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

The MIN_TOKEN_TTL_S = 5 floor (client.ts:75) guards against an expires_in smaller than TOKEN_REFRESH_MARGIN_S causing expiresAtMs to land in the past, which would re-mint on every request. No test exercises this path. A test with tokenOk({ expires_in: 30 }) and two sequential requests — asserting one auth/token call — would pin it.

medium

No test for natural token expiry path (tokenState exists but expired)

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

The getToken() guard `tokenState.expiresAtMs > Date.now()` branches on expiry but no test exercises the 'cached token has expired, re-mint' path. Use vi.setSystemTime / fake timers to advance past expiresAtMs and confirm a fresh mint fires.

medium

Smoke test param-name assertions cover only 4 of 33 tools; new tools unguarded

packages/mcp-server/src/tools-list.smoke.test.ts:62

The original audit found a param-name bug (period_start vs period_start_from). The smoke test guards 4 tools against this, but the 21 new tools from this diff (batu_whoami, batu_get_energy_summary, batu_get_savings, asset-management suite, batu_provision_assets) have only a count-floor check. At minimum add presence + a key param assertion for: batu_get_energy_summary (site_public_id not site_id), batu_provision_assets (sites array), batu_get_savings (from/to/site_public_id).

low

listTools() swallows JSON-RPC error responses — test silently times out instead of failing fast

packages/mcp-server/src/tools-list.smoke.test.ts:43

If the server returns `{ id: 2, error: {...} }` (no `result` key), `m.result.tools` throws inside the stdout event handler — uncaught, so the Promise never resolves. The test then waits the full 15s timeout. Add: `if (m.error) { clearTimeout(timer); child.kill(); rej(new Error(JSON.stringify(m.error))); return; }` before accessing m.result.

low

beforeAll execSync stdio:'ignore' makes build failures produce cryptic test errors

packages/mcp-server/src/tools-list.smoke.test.ts:17

A TypeScript compile error causes execSync to throw with only 'Command failed: pnpm run build' — no compiler output visible. The subsequent test failure reads 'timed out waiting for tools/list'. Use stdio: ['ignore', 'inherit', 'inherit'] to surface build errors in the test run output.

info

console.error spy in results.test.ts is noise suppression only, not load-bearing

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

vi.spyOn(console, 'error').mockImplementation(() => {}) prevents test output noise but has no corresponding expect() assertion on the calls. The tests pass without the spy; it's purely cosmetic. Fine as-is.

improvement3

low

batu_explore_integration description omits live vendor API call latency

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

The /explore endpoint calls the vendor API synchronously via SFN. The description says 'does not persist anything' (accurate) but gives no latency hint. Add: 'Makes a live call to the vendor API — may take several seconds.' Agents that retry aggressively on slow responses could hit rate limits.

low

tools-list.smoke.test.ts magic count 33 needs a comment explaining >= semantics

packages/mcp-server/src/tools-list.smoke.test.ts:62

expect(tools.length).toBeGreaterThanOrEqual(33) — no comment explains that 33 is the current tool count, why >= (not ===) is used, or that the number must be bumped if a tool is intentionally removed. One comment line would make the maintenance obligation clear.

info

batu_get_energy_summary granularity enum has one value — consider omitting the param

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

z.enum(['none']).optional() is accurate but may confuse agents that try to vary it. If the API defaults to 'none' when omitted, the param could be dropped until more granularity values land. The .describe() text already warns it's the only option, which is good.

History · 5 commits

  1. 59a73bdneeds attentionincremental1H · 6M · 5L2026-07-10 05:23current
  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:13
  5. ee41aeaneeds attentionfull5H · 6M · 11L2026-07-09 19:12