Skip to Content
Deneva MCPComponentsMcp Tools

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:

  1. If input.platform is not in supportedPlatforms → audit mcp.tool_failed with reason: 'unsupported_platform', return { error: 'unsupported_platform', platform }.
  2. Otherwise resolve the adapter via registry.getAdapter, run readOrFetch with the cache key (tenantId, platform, reportType, dateRange), and return { data, cache: 'hit' | 'miss' }. Audits mcp.tool_called with 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

ToolReport typePhase 2 platforms
get_account_healthaccount_healthgoogle
get_search_term_wastesearch_term_wastegoogle
get_quality_scorequality_scoregoogle
get_auction_insightsauction_insightsgoogle
get_pmax_breakdownpmax_breakdowngoogle
get_budget_optimizerbudget_optimizergoogle
get_weekly_anomalyweekly_anomalygoogle

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 a hit; both write mcp.tool_called audit rows with the right cache metadata.
  • unsupported_platform: every Phase 2 tool returns the error envelope without invoking the adapter when called with meta or tiktok; each writes one mcp.tool_failed audit 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.rows array survives the cache write/read.
  • MCP transport E2E: tools/list returns the expected eight tool names (ping + 7); tools/call get_account_health returns { 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.