Reduce MCP tool registration overhead by collapsing 31 of the 32 action tools into 4 namespace-scoped gateway tools, while keeping sdl.action.search and sdl.info available as universal discovery and diagnostics surfaces.
The tool gateway consolidates 31 of the 32 action tools into 4 typed proxy tools (sdl.query, sdl.code, sdl.repo, sdl.agent). sdl.file.read is flat-only and not gateway-routed. Each gateway tool accepts an action field that routes the call to the appropriate handler and then applies the original per-tool validation. sdl.action.search and sdl.info stay registered outside the gateway so discovery and environment diagnostics remain available in every mode.
When an MCP client connects, it calls tools/list to discover available tools. The response includes tool names, descriptions, and JSON schemas. Registering 32 action tools separately is expensive, especially once titles, richer descriptions, and action-specific schema metadata are included.
Without gateway:
tools/list → 34 tools
= 32 action tools + sdl.action.search + sdl.info
With gateway:
tools/list → 6 tools
= 4 gateway tools + sdl.action.search + sdl.info
The gateway measurement script compares the 31 gateway-routable tools against the 4 gateway tools, because those are the surfaces being consolidated. `sdl.file.read` is flat-only. The two universal tools are present in both modes.
This matters because:
- Agents process
tools/listat the start of every conversation - Tokens spent on tool schemas are tokens not available for code context
- Large tool registrations cause some MCP clients to truncate or error
- Fewer tools means fewer selection decisions for the agent (faster + more accurate)
┌─────────────────────────────────────────────────────┐
│ MCP Server │
│ │
│ sdl.repo.register sdl.symbol.search │
│ sdl.repo.status sdl.symbol.getCard │
│ sdl.repo.overview sdl.symbol.getCards │
│ sdl.index.refresh sdl.slice.build │
│ sdl.buffer.push sdl.slice.refresh │
│ sdl.buffer.checkpoint sdl.slice.spillover.get │
│ sdl.buffer.status sdl.delta.get │
│ sdl.code.needWindow sdl.policy.get │
│ sdl.code.getSkeleton sdl.policy.set │
│ sdl.code.getHotPath sdl.pr.risk.analyze │
│ sdl.agent.context sdl.context.summary │
│ sdl.agent.feedback sdl.agent.feedback.query │
│ sdl.runtime.execute sdl.memory.store │
│ sdl.runtime.queryOutput sdl.memory.query │
│ sdl.memory.remove sdl.memory.surface │
│ sdl.usage.stats │
│ │
│ 31 tools × full JSON schema │
│ ~4,000+ tokens total │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ MCP Server │
│ │
│ sdl.query → 9 actions (symbol.*, slice.*, etc.) │
│ sdl.code → 3 actions (code.*) │
│ sdl.repo → 7 actions (repo.*, index.*, policy.*, │
│ usage.stats) │
│ sdl.agent → 12 actions (agent.*, buffer.*, runtime,│
│ memory.*) │
│ │
│ 4 tools × compact schema + compact desc │
│ ~713 tokens total │
└─────────────────────────────────────────────────────┘
flowchart LR
Agent["Agent Call"]
subgraph "Gateway Layer"
GW{"Which gateway<br/>tool?"}
Q["sdl.query<br/>(9 actions)"]
C["sdl.code<br/>(3 actions)"]
R["sdl.repo<br/>(7 actions)"]
A["sdl.agent<br/>(12 actions)"]
end
subgraph "Validation"
Thin["Thin Schema<br/>(first pass)"]
Router["Router: extract action<br/>+ merge repoId"]
Strict["Original Zod Schema<br/>(strict second pass)"]
end
Handler["Same Handler<br/>as Flat Mode"]
Agent --> GW
GW --> Q & C & R & A
Q & C & R & A --> Thin
Thin --> Router
Router --> Strict
Strict --> Handler
style Agent fill:#cce5ff,stroke:#004085
style Handler fill:#d4edda,stroke:#28a745
The 31 action tools are organized into 4 namespaces:
| Gateway Tool | Actions | Domain |
|---|---|---|
sdl.query |
9 | Read-only intelligence: symbol search/cards, slices, deltas, summaries, PR risk |
sdl.code |
3 | Gated code access: needWindow, skeleton, hotPath |
sdl.repo |
7 | Repository lifecycle: register, status, overview, index, policy, usage stats |
sdl.agent |
12 | Agentic ops: context, feedback, buffers, runtime, runtime.queryOutput, memory |
Outside the gateway, SDL-MCP always keeps:
sdl.action.searchfor action discoverysdl.infofor runtime and environment diagnostics
Each gateway tool uses an action-based union schema on the action field. Some actions add a second validation layer for mutually exclusive inputs such as symbolId versus symbolRef. The calling pattern is:
// Instead of:
{ "tool": "sdl.symbol.search", "args": { "repoId": "x", "query": "auth" } }
// Gateway mode:
{ "tool": "sdl.query", "args": { "repoId": "x", "action": "symbol.search", "query": "auth" } }The repoId field is hoisted to the envelope level (shared across all actions in a namespace), and the action field selects which handler processes the call.
For symbol-card actions, gateway mode now accepts the same natural-identifier shape as flat mode:
{
"tool": "sdl.query",
"args": {
"repoId": "x",
"action": "symbol.getCard",
"symbolRef": { "name": "handleRequest", "file": "src/server.ts" }
}
}symbol.getCards follows the same pattern with symbolRefs. Mixed natural-reference batches can return partial-success metadata instead of failing the whole request.
Gateway requests also go through the shared normalization layer before validation, so common aliases and snake_case forms work the same way they do in flat mode:
{
"tool": "sdl.query",
"args": {
"repo_id": "x",
"action": "slice.build",
"task_text": "trace auth flow",
"edited_files": ["src/auth/token.ts"]
}
}Aliases such as repo, repo_id, root_path, project_path, symbol_id, symbol_ids, from_version, to_version, slice_handle, spillover_handle, if_none_match, known_etags, known_card_etags, failing_test_path, edited_files, entry_symbols, relative_cwd, and identifiers normalize to the canonical field names before strict validation.
Validation happens in two passes for safety:
Agent Call
│
▼
┌─────────────────────┐
│ Gateway Schema │ Discriminated union on `action`
│ (cheap first-pass) │ Catches wrong action names, type errors
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Router │ Extracts action, merges repoId
│ │ Looks up handler from ActionMap
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Original Zod Schema │ Strict second-pass validation
│ (per-handler) │ Identical to flat-mode validation
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Handler Function │ Same handler as flat mode
│ │ Zero behavioral difference
└─────────────────────┘
The key to token savings is the compact schema emitted in tools/list responses. SDL-MCP no longer publishes a description-free envelope with only repoId and action. Instead, each gateway tool exposes a compact oneOf schema with action-specific fields, while still preserving user-facing descriptions, required-vs-optional visibility, and default semantics where they help the agent construct valid calls.
That means gateway mode keeps most of the usability benefits of flat mode while still compressing the registration surface down to 4 namespace tools.
Measured with the included token measurement script (scripts/measure-gateway-schema-tokens.ts):
| Mode | Tools | Characters | Est. Tokens |
|---|---|---|---|
| Flat core action tools | 31 | ~17,000 | ~4,250 |
| Gateway core namespace tools | 4 | ~2,900 | ~725 |
| Gateway-only runtime surface | 6 | includes universal tools | varies slightly |
| Hybrid runtime surface | 36 | includes universal tools | varies slightly |
| Metric | Value |
|---|---|
| Token reduction | ~83% (4,250 → 725) |
| Tokens saved per conversation | ~3,525 |
| Character reduction | ~17,000 → ~2,900 |
| Tool count reduction | 31 → 4 |
The savings come from three techniques:
- Fewer tools — 4 vs 31 registration entries
- Compact action-aware schemas — gateway
oneOfvariants preserve useful field metadata without publishing every full flat schema separately - $defs deduplication — repeated sub-schemas are hoisted to
$defswith$refpointers (viacompact-schema.ts)
Gateway mode is controlled in your SDL-MCP config file:
enabled |
emitLegacyTools |
Tools Registered | Use Case |
|---|---|---|---|
true |
true |
38 (4 gateway + 32 action + 2 universal) | Migration period — agents can use either style |
true |
false |
6 (4 gateway + 2 universal) | Maximum registration savings |
false |
— | 34 (32 flat + 2 universal) | Backward compatibility, legacy agents |
Legacy tools include a deprecation notice in their description:
[Legacy — prefer sdl.query] Search for symbols by name or summary
src/gateway/
index.ts # Registration orchestrator — registers 4 gateway + optional legacy
router.ts # Action routing — maps action names to { schema, handler } pairs
schemas.ts # Full Zod schemas — action-based unions per namespace
thin-schemas.ts # Compact action-aware schemas for tools/list
descriptions.ts # Compact tool descriptions — action reference cards
compact-schema.ts # $defs/$ref deduplicator for schema optimization
legacy.ts # Legacy tool aliases with deprecation notices
// src/gateway/index.ts
export function registerGatewayTools(server, services, config) {
const actionMap = createActionMap(services.liveIndex);
// Register 4 gateway tools with compact action-aware schemas
server.registerTool("sdl.query", QUERY_DESCRIPTION, QueryGatewaySchema,
handler, QUERY_THIN_SCHEMA);
server.registerTool("sdl.code", CODE_DESCRIPTION, CodeGatewaySchema,
handler, CODE_THIN_SCHEMA);
server.registerTool("sdl.repo", REPO_DESCRIPTION, RepoGatewaySchema,
handler, REPO_THIN_SCHEMA);
server.registerTool("sdl.agent", AGENT_DESCRIPTION, AgentGatewaySchema,
handler, AGENT_THIN_SCHEMA);
// Optional: also register 32 flat tool names
if (config.emitLegacyTools) {
registerLegacyTools(server, services);
}
}The gateway router (src/gateway/router.ts) performs the core dispatch:
export async function routeGatewayCall(rawArgs, actionMap, ctx) {
const normalized = normalizeToolArgs(rawArgs);
const { action, repoId, ...rest } = normalized;
// Look up handler by action name
const entry = actionMap[action];
if (!entry) throw new Error(`Unknown gateway action: ${action}`);
// Merge repoId back into params for handler compatibility
const merged = repoId !== undefined ? { repoId, ...rest } : rest;
// Second-pass validation using the original strict Zod schema
const parsed = entry.schema.parse(merged);
return entry.handler(parsed, ctx);
}The compact-schema.ts module optimizes JSON Schemas for token efficiency:
- Fingerprint sub-schemas — canonicalize and hash every object node
- Deduplicate — sub-schemas appearing 2+ times with size >40 chars are hoisted to
$defsand replaced with$refpointers - Preserve useful metadata — keep action-specific descriptions and defaults that help agents call the gateway correctly
Example deduplication:
// Before: repeated { "type": "number", "minimum": 0, "maximum": 1 } appears 4 times
// After: hoisted to $defs/d0, referenced as { "$ref": "#/$defs/d0" }The gateway router is also used by the sdl-mcp tool command for direct CLI access. The CLI dispatcher calls createActionMap() directly, bypassing the MCP server entirely while sharing the same handler map.
See CLI Tool Access for full CLI documentation.
If your agent configuration currently uses the flat tool names (e.g., sdl.symbol.search), you have two options:
- Keep legacy tools — Set
emitLegacyTools: truein your config and both flat and gateway tools are available (note: legacy tools are deprecated) - Switch to gateway — Update your agent instructions to use
sdl.querywithaction: "symbol.search"instead ofsdl.symbol.search
Update your CLAUDE.md / AGENTS.md to use gateway-style calls:
# Before (flat tools)
Use `sdl.symbol.search` to find symbols.
Use `sdl.code.getSkeleton` to see code structure.
# After (gateway tools)
Use `sdl.query` with `action: "symbol.search"` to find symbols.
Use `sdl.code` with `action: "code.getSkeleton"` to see code structure.If you need backward compatibility with older MCP clients:
{
"gateway": {
"enabled": false
}
}This registers the 32 flat tools plus the universal sdl.action.search and sdl.info surfaces.
Run the included measurement script to verify savings for your configuration:
node --experimental-strip-types scripts/measure-gateway-schema-tokens.tsOutput:
=== SDL-MCP Gateway Schema Token Measurement ===
Flat mode: 32 tools, ~4550 tokens (~18200 chars)
Gateway mode: 4 tools, ~725 tokens (~2900 chars)
Hybrid mode: 34 tools
Gateway is ~17% of flat mode
Estimated savings: ~3525 tokens per tools/list call
✅ Gateway schema is within target (≤40% of flat)
Gateway mode optimizes tool registration overhead. For context retrieval, sdl.agent.context provides the most token-efficient path with automatic rung selection and adaptive symbol ranking (contextMode: "precise" for targeted lookups, "broad" for exploration). In Code Mode, sdl.context provides the same retrieval surface, while sdl.workflow is reserved for multi-step operations such as runtime execution, data transforms, and batch mutations.
{ "gateway": { // Enable gateway mode (default: true) "enabled": true, // Also emit the 32 flat tool names for backward compat (default: false, deprecated) "emitLegacyTools": false } }