Skip to content
Domain Facade Pattern for MCP: Reducing 60 Tools to 9 Cuts LLM Context by 85%

Domain Facade Pattern for MCP: Reducing 60 Tools to 9 Cuts LLM Context by 85%

The article presents a Domain Facade design pattern for MCP servers that consolidates 60+ fine-grained tools into 9 domain-level tools, reducing per-interaction token consumption from ~12,000 to ~2,000 (85% reduction) while preserving full functionality, and introduces a manifest-based roundtripping pattern for atomic multi-file edits.

Linyb Geek Road
Linyb Geek Road
Linyb Geek Road

Introduction

REST APIs are designed for human developers who read documentation once and remember endpoints. However, modern API consumers are LLMs with 200k-token context windows that must re-read every tool description on every interaction — and you pay for each token.

We reduced 60 tools to 9. Functionality unchanged. Context overhead cut by 85%.

Key insight: Every interaction, all tool descriptions are re-read. This demands a new design philosophy.

Core Problem: Tool Proliferation

MCP (Model Context Protocol) lets us extend AI assistants with custom tools, but a common pitfall is over-fragmenting interfaces. For "memory" alone, teams create dozens of tools:

memory_add (add memory)
memory_get (get memory)
memory_list (list memories)
memory_update (update memory)
memory_delete (delete memory)
memory_pin (pin memory)
memory_archive (archive memory)
memory_link (link memories)
memory_unlink (unlink memories)
memory_search (search memories)
memory_embed (generate memory vectors)
...

Replicated across projects, tasks, docs, files, databases, this quickly yields 60+ tools. Each needs a description, parameter schema, and examples — all consuming tokens.

Result: LLM processes ~12,000 tokens of tool descriptions every interaction.

Consequences: slower responses, soaring costs, and the AI confuses similar tools (e.g., picking memory_update instead of memory_upsert).

Real-World Before/After Comparison

Version 1: Fragmented Design (excerpt)

60+ tools defined individually, each with name, description, and inputSchema. Example tools: MemoriesAdd — "Add a new memory to the system" with 13 properties (projectKey, title, body, scope, memoryType, tags, importance, pinned, ttlIso, userId, chatId, sourceKind, sourceRef) MemoriesSearch — "Search memories using hybrid FTS + semantic search" MemoriesList, MemoriesGet, MemoriesUpdate, MemoriesPin, MemoriesArchive, MemoriesDelete, MemoriesLink, MemoriesUnlink, MemoriesRelated, MemoriesPrune, MemoriesEmbed,

MemoriesStats
ProjectsList

,

ProjectsGet
DocsList

,

DocsSearch
FilesList

, FilesRead,

FilesWrite
DbTables

, DbQuery, DbExec... 35+ more

~12,000 tokens per interaction. Every single time.

Version 2: Domain Facade Design (complete)

9 tools, each a domain entry point with sub-commands:

{
  "tools": [
    {
      "name": "MemoryExecute",
      "description": "Neural memory system. Commands: add, get, list, search, update, pin, delete, archive, link, unlink, related, embed, stats, prune",
      "inputSchema": {
        "type": "object",
        "properties": {
          "cmd": {"type": "string"},
          "detail": {"enum": ["minimal", "standard", "full"]},
          "params": {"type": "object"}
        },
        "required": ["cmd"]
      }
    },
    {"name": "ProjectsExecute", "description": "Project management. Commands: list, get, upsert, archive, stats, get_tree"},
    {"name": "TasksExecute", "description": "Task tracking. Commands: list, get, upsert, delete, set_status, add_note"},
    {"name": "DocsExecute", "description": "Document management. Commands: list, get, upsert, delete, search, pin, embed"},
    {"name": "FilesExecute", "description": "File operations. Commands: list, get, put, delete, mkdir, roundtrip_*"},
    {"name": "DatabaseExecute", "description": "SQL access. Commands: query, exec, schema, tables, stats"},
    {"name": "ArtifactsExecute", "description": "Content-addressable storage. Commands: get, search, upsert"},
    {"name": "HydrationExecute", "description": "AI context. Commands: hydrate, persona_*, identity_*, preferences_*"},
    {"name": "DeepSearch", "description": "External search: Google, GitHub, Wikipedia, Hacker News"}
  ]
}

~2,000 tokens per interaction. Full functionality preserved.

Core Design: Domain Encapsulation, One Tool per Domain

Original 14 memory tools → 1 tool with 14 commands:

// Before: 14 tools, 14 descriptions, 14 param schemas
MemoriesAdd({ title, body, ... })  // add memory
MemoriesSearch({ query, topK, ... })  // search memory
MemoriesPin({ id, pinned })  // pin memory
...

// After: 1 tool, 1 description, command as parameter
MemoryExecute({ cmd: "add", params: { title, body, ... }})  // execute add command
MemoryExecute({ cmd: "search", params: { query, topK, ... }})  // execute search command
MemoryExecute({ cmd: "pin", params: { id, pinned }})  // execute pin command

AI now reasons over 9 domains instead of 60 scattered verbs. To "search memory", it calls MemoryExecute with cmd: "search" — simple and direct.

Implementation Details

Each domain follows a unified structure (C# example):

/// Domain command execution entry
/// command: Domain command object (command name + params)
/// returns: Domain operation response
public async Task<DomainResponse> ExecuteAsync(DomainCommand command)
{
    // Lowercase command name, dispatch to handler
    return command.Cmd.ToLowerInvariant() switch
    {
        "add" => await AddAsync(command),     // handle add
        "get" => await GetAsync(command),     // handle get
        "list" => await ListAsync(command),   // handle list
        "search" => await SearchAsync(command), // handle search
        "update" => await UpdateAsync(command), // handle update
        "delete" => await DeleteAsync(command), // handle delete
        _ => DomainResponse.Failure(command.Cmd, "Unknown command") // unknown command
    };
}

Unified Request/Response Format

Request example:

{
  "cmd": "search",
  "detail": "standard",
  "params": {
    "projectId": 1,
    "query": "authentication",
    "topK": 10
  }
}

Response example:

{
  "ok": true,
  "cmd": "search",
  "data": [...],
  "count": 10,
  "error": null
}

Echoing the command name in the response is critical — when AI runs multiple operations concurrently, it can match each response to its request.

Configurable Detail Level

A single parameter controls response verbosity, avoiding unnecessary tokens:

minimal — Only ID, title. Use case: List views, counts, quick validation.

standard — Core fields, content summary. Use case: General daily use.

full — All fields and content. Use case: Deep inspection, debugging.

AI picks the appropriate level — e.g., minimal when only a count is needed, skipping 50KB of full payload.

Final 9 Core Tools

MemoryExecute — Commands: add, get, list, search, update, pin, delete, link, unlink, embed, stats, prune. Purpose: Neural memory with hybrid search.

ProjectsExecute — Commands: list, get, upsert, archive, stats, get_tree. Purpose: Workspace/project management.

TasksExecute — Commands: list, get, upsert, delete, set_status, add_note. Purpose: Task tracking.

DocsExecute — Commands: list, get, upsert, delete, search, pin, embed. Purpose: Document management.

FilesExecute — Commands: list, get, put, delete, mkdir, roundtrip_*. Purpose: File operations.

DatabaseExecute — Commands: query, exec, schema, tables, stats. Purpose: Direct SQL database access.

ArtifactsExecute — Commands: get, search, upsert. Purpose: Content-addressable storage (CAS).

HydrationExecute — Commands: hydrate, persona_*, identity_*, preferences_*. Purpose: AI context loading (persona, identity, preferences).

DeepSearch — Aggregate command. Purpose: External search: Google, GitHub, Wikipedia, Hacker News.

60+ operations → 9 tools. Zero functionality lost.

Why This Design Works

Lowers AI cognitive load — AI identifies 9 domains, not 60 verbs; simpler decisions.

Uniform interface style — Learn one format, apply to all domains. Every domain has list / get / search etc., consistent request/response/error formats.

Extreme token savings — "Memory" description written once, not 14 times; massive context reduction.

Flexible extensibility — New command = one switch branch; no tool re-registration, schema changes, or doc updates.

Fewer wrong-tool selections — 9 choices vs 60; AI no longer confuses MemoriesUpdate with MemoriesUpsert.

Key Metrics Comparison

Tool list token cost : Before (60 tools) ~12,000 → After (9 tools) ~2,000

Wrong-tool probability : Before frequent → After near zero

Response latency : Before higher → After lower

Monthly API cost : Before high ($$$) → After low ($)

Bonus Technique: Manifest-Based Roundtripping for Atomic Multi-File Edits

Original Pain Point

LLM edits files sequentially:

PUT /file/a.cs → write content
PUT /file/b.cs → write content
PUT /file/c.cs → write content

Three API calls, no atomicity (all-or-nothing), no conflict detection — if user modifies a file during AI editing, AI silently overwrites user changes.

Solution: Roundtrip Editing Flow

// Step 1: Start edit: roundtrip_start({ paths: ["a.cs", "b.cs", "c.cs"] })
roundtrip_start({ paths: ["a.cs", "b.cs", "c.cs"] })
  → Returns: manifest (original file hashes) + ZIP of original files

// Step 2: AI edits files inside ZIP locally (no direct file ops)
[AI edits files in ZIP]

// Step 3: Preview changes: roundtrip_preview({ manifestId, modifiedZip })
roundtrip_preview({ manifestId, modifiedZip })
  → Returns: diff + conflict warnings (if any)

// Step 4: Commit: roundtrip_commit({ manifestId, zip, mode: "replace" })
roundtrip_commit({ manifestId, zip, mode: "replace" })
  → Atomic apply: all changes succeed or none do

Core: Manifest Records Original State

The manifest is a "file snapshot credential" recording each file's path, SHA256 hash (content fingerprint), and size:

{
  "manifestId": "rtp_2024-01-15T10-30-00Z_a1b2c3d4",
  "entries": [
    {"path": "src/auth/login.cs", "sha256": "abc123...", "size": 2048},
    {"path": "src/auth/logout.cs", "sha256": "def456...", "size": 1024}
  ]
}

SHA256 is critical — any content change produces a completely different hash.

Conflict Detection on Commit

On commit, server verifies files haven't been tampered with:

// Compute current server file hash ("current fingerprint")
var currentSha256 = ComputeHash(physicalPath);
// Compare manifest's "original fingerprint" with "current fingerprint"
if (currentSha256 != manifestEntry.Sha256)
  // Mismatch → file modified externally, add conflict warning
  conflicts.Add($"File modified externally: {virtualPath}");

Three Commit Modes

replace (full replace) — Existing files: overwrite content. New files: create. Use case: Fully sync AI modifications (e.g., refactored files).

add_only (add only) — Existing files: skip. New files: create. Use case: Safe scaffolding (e.g., add config files, don't touch existing code).

update_only (update only) — Existing files: overwrite content. New files: skip. Use case: Targeted fixes (e.g., bug fixes in existing files, no new files).

Benefits: Single atomic operation (all-or-nothing), bandwidth efficient (ZIP vs individual files), no data loss (conflicts warned upfront). The manifest serves as a "safety checkpoint" — you know exactly which file versions you started from.

When Not to Use This Pattern

Simple servers — If only 3-5 tools, unnecessary complexity.

Stateless tools — Independent operations (e.g., "generate random number", "format as JSON") don't need file state tracking.

Developer-facing APIs — Programmers prefer fine-grained file operations (e.g., REST); this packaging reduces flexibility.

This pattern is designed specifically for LLM constraints: limited context window, per-token cost, need to minimize "thinking burden".

Summary

MCP is emerging; best practices are still forming. But one thing is clear: Interfaces designed for human developers are not necessarily suitable for LLMs.

Humans read docs once and remember; LLMs re-read every conversation. Humans remember different endpoints; LLMs pay per character. Humans like granular options; LLMs get confused by 60 similar verbs.

"Context-Optimized APIs" flip traditional design: instead of "does this follow REST?", ask "how to minimize LLM context overhead while preserving functionality?"

Our answer: Domain Facades — one tool per domain, command as parameter, unified data formats, configurable detail levels.

Result: 60 tools → 9 tools; 12,000 tokens → 2,000 tokens; functionality unchanged. AI responds faster, costs less, selects tools more accurately.

Sometimes the best interface design is the one that best respects the constraints of its user — here, the LLM.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.