MCP tools
Source: src/mcp/tools/ — _helpers.ts template plus one file per tool.
Phase 1 shipped two stub tools (ping, get_account_health stub). Phase 2 PR-6 replaced the stub with seven real Google-backed tools, all driven through a single makeTool factory. Phase 3 will broaden each tool’s supportedPlatforms to include meta and tiktok where their underlying API exists.
The makeTool factory
File: src/mcp/tools/_helpers.ts
Every platform tool uses the same factory, so cache lookup, adapter dispatch, audit logging, and unsupported-platform handling are written once:
makeTool({
name: 'get_account_health',
description: '...',
inputSchema: BaseInputSchema, // closed enums; no free text
reportType: 'account_health', // cache key segment + TTL lookup
supportedPlatforms: ['google'], // Phase 3 broadens this
fetcher: (adapter, input, tenantId) =>
adapter.fetchAccountHealth(tenantId, input.dateRange),
});The generated handler:
- If
input.platformis not insupportedPlatforms→ auditmcp.tool_failedwithreason: 'unsupported_platform', return{ error: 'unsupported_platform', platform }. - Otherwise resolve the adapter via
registry.getAdapter, runreadOrFetchwith the cache key(tenantId, platform, reportType, dateRange), and return{ data, cache: 'hit' | 'miss' }. Auditsmcp.tool_calledwith the cache outcome.
The MCP wrapper in server.ts only catches THROWN errors — success audits happen inside the handler so the cache field can be recorded.
The seven Phase 2 tools
| Tool | Report type | Phase 2 platforms |
|---|---|---|
get_account_health | account_health | |
get_search_term_waste | search_term_waste | |
get_quality_score | quality_score | |
get_auction_insights | auction_insights | |
get_pmax_breakdown | pmax_breakdown | |
get_budget_optimizer | budget_optimizer | |
get_weekly_anomaly | weekly_anomaly |
Each file is ~15 lines: imports makeTool and BaseInputSchema, exports a single tool definition. See e.g. account-health.ts.
The ping tool
File: src/mcp/tools/ping.ts
Empty input. Returns { ok: true, tenantId, requestId, serverTime }. Does not use makeTool — it has no platform / cache / adapter concerns. Stays in the codebase as a synthetic health probe for external monitors.
curl -X POST http://127.0.0.1:3001/mcp \
-H "X-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping","arguments":{}}}'Closed-enum input design
BaseInputSchema in _helpers.ts:
{
platform: 'google' | 'meta' | 'tiktok',
dateRange: 'last_7_days' | 'last_30_days' | 'last_90_days',
}No free-text fields. This is intentional:
- Eliminates a class of injection vectors (parameter pollution, command-injection through tool args).
- Keeps the test surface tiny — 3 platforms × 3 ranges = 9 combinations.
When Phase 3+ adds tools that take e.g. campaign IDs, those will be UUIDs (Zod .uuid()), still strictly typed.
Transport mode
server.ts builds a fresh McpServer + StreamableHTTPServerTransport per HTTP request. The transport runs in stateless mode (sessionIdGenerator: undefined) — each POST /mcp is independent and self-contained. With a stateful sessionIdGenerator the SDK rejects every non-initialize request with “Server not initialized”, which would break the single-shot tools/call curl pattern documented in setup-ubuntu.md. Phase 1 set the field to randomUUID() but built fresh transports per request anyway — that latent bug was fixed in PR-6.
Adding a new tool
1. Create src/mcp/tools/your-tool.ts:
- Import { BaseInputSchema, makeTool } from './_helpers.js';
- Export const yourTool = makeTool({ name, description, inputSchema,
reportType, supportedPlatforms, fetcher });
2. If a new reportType: add to src/cache/ttl-config.ts.
3. If the adapter doesn't already expose a fetch method: add it to
adapter.interface.ts (cross-platform) and implement in each adapter.
4. Append yourTool to the TOOLS array in src/mcp/server.ts.
5. Add a per-tool test entry to tests/integration/tools-google.test.ts.The wrapper handles audit logging, schema validation, cache lookup, and transport plumbing for you.
Tests
tests/integration/tools-google.test.ts covers:
- Cache + audit: first call is a
miss, second is ahit; both writemcp.tool_calledaudit rows with the rightcachemetadata. - unsupported_platform: every Phase 2 tool returns the error envelope without invoking the adapter when called with
metaortiktok; each writes onemcp.tool_failedaudit row. - Per-tool happy paths: one entry per of the seven tools — feeds canned GAQL rows through the adapter and asserts the Zod-validated
data.rowsarray survives the cache write/read. - MCP transport E2E:
tools/listreturns the expected eight tool names (ping + 7);tools/call get_account_healthreturns{ cache: 'miss', data: {...} }and writes the expected audit row.
Mock boundary is google-ads-api (via vi.mock + vi.hoisted); the cache, audit log, encryption, and Fastify path all run for real.