Skip to content

Tenant-Scoped Tool Subsetting (Phase 1)

ADR-039 — Tenant-Scoped Tool Subsetting (Phase 1)

Status: Accepted
Date: 2026-05-06
Author: Claude Code / Mishaal Murawala
Invariant impact: Invariant 7 (34 MCP tools hard ceiling) — no change to ceiling; changes how tools are surfaced per tenant


Context

V5 currently registers all 34 MCP tools for every authenticated request, regardless of which providers the tenant has connected. A tenant that only uses Google Ads + HubSpot receives a tool list that includes Salesforce, LinkedIn Ads, DealCloud, AWS, etc. This creates three problems:

  1. Context pollution — AI clients (Claude, Cursor) see irrelevant tools, which increases the likelihood of hallucinated tool calls to unconnected providers.
  2. Token waste — MCP tools/list responses grow proportional to the number of registered tools. At 34 tools with rich description fields, the payload is non-trivial.
  3. Security surface — A misconfigured token in KV could theoretically be called for a provider the tenant never intended to connect.

tenant_config:{tenant} in KV already stores a connected_apis?: string[] field (typed in TenantConfig). The data exists. This ADR wires it to tool registration.


Decision

Phase 1 (this ADR): filter tool registration at McpServer init time.

At request entry in src/handlers/mcp.ts, tenantConfig is already fetched from KV (line 74) at zero additional cost. Phase 1 adds a TOOL_REGISTRY constant — an array of { register, provider } entries — and replaces the 34 individual registerXxx() calls with a single loop:

const connectedApis = tenantConfig.connected_apis; // string[] | undefined
for (const { register, provider } of TOOL_REGISTRY) {
if (provider === null || !connectedApis || connectedApis.includes(provider)) {
register(server, getContext);
}
}

Provider mapping rules:

  • provider: null → platform / meta tools. Always registered. These have no external connection requirement (call_api, discover_apis, agent_state, batch, claude, llm_invoke, ai_invoke, search_knowledge, context_query, context_explain, web_fetch, submit_feedback).
  • provider: 'google' → covers Google Ads, GA4, GSC, Gmail, Google Calendar, and Gemini (all use the same OAuth connection).
  • provider: 'microsoft' → covers Microsoft Ads and Microsoft Calendar (same tenant OAuth).
  • provider: 'aws' → covers all four AWS tools (Bedrock, Bedrock Converse, Textract, SES, Nova Canvas — same IAM credentials).
  • provider: 'salesforce' → covers both salesforce_query and salesforce_crm.

Backward compatibility: If connected_apis is absent (tenants provisioned before this ADR, or admin-created tenants that haven’t been updated), the condition !connectedApis is true → all tools are registered. Identical to prior behaviour. Zero breaking changes.


Alternatives Considered

Phase 2: Runtime filtering at tools/list response level

Instead of skipping registerTool() calls, register all tools but filter the tools/list response. Rejected for Phase 1 because: (a) the SDK provides no hook for this without forking McpServer; (b) tools registered but not listed can still be called if a client sends tools/call directly — filtering at list-response level doesn’t prevent that.

Phase 3: OAuth-scope-driven filtering

Use the OAuth scopes from the bearer token to derive which providers are connected, rather than tenant_config.connected_apis. More elegant long-term (single source of truth = the token) but requires scope design work and is out of scope for Phase 1.


Consequences

Positive:

  • Tenants with connected_apis set see only relevant tools → cleaner AI context window.
  • tools/list response size reduced proportionally (e.g. a Google-only tenant gets ~10 tools instead of 34).
  • No cold path changes, no KV read count increase, no latency impact.

Negative / risk:

  • connected_apis must be kept in sync when a tenant adds a new provider. If a tenant connects HubSpot but connected_apis still lists only ['google'], HubSpot tools won’t appear. Mitigation: the Nango webhook handler (ADR-038) is the natural place to update connected_apis on connection creation — Phase 2 work.
  • Phase 1 is “opt-in via config” rather than “automatic”. Until connected_apis is populated for all tenants, the benefit is only realized for explicitly configured tenants.

Implementation

Single file change: src/handlers/mcp.ts

  • Add TOOL_REGISTRY const (54 lines, including comments) above mcpHandler declaration.
  • Replace 34 registerXxx(server, getContext) calls with 4-line loop.
  • Net: ~-30 LOC.

No migration needed. No KV schema change. No D1 change.


  • ADR-038: Nango token management (the write path that populates connected_apis will eventually live here)
  • TOOLS.md: Provider-to-tool mapping table
  • Invariant 7: tool ceiling (34 hard limit, ceiling 35) — this ADR does not change the ceiling