Taming MCP Tool Explosion: Slimming DeepSeek Harness Context After Adding REA
The author details how adding REA's 130 MCP tools alongside OpenPencil's 150 tools overwhelmed a 256K context window, and explores three optimization strategies — per-project tool filtering, lazy loading via Tool Search, and a custom Meta-MCP proxy — plus REA-specific schema reductions like Evidence ID references and split union types.
The article begins with a concrete problem: after configuring the Reverse Engineering Agent (REA) via MCP in DeepSeek Harness (DSH), the combined tool count from REA (~130 tools) and OpenPencil (~150 tools) reached nearly 280 tools. Each tool carries a JSON Schema with descriptions, parameters, nested objects, enums, and constraints. In the author's self-hosted Qwen3-8B-Flash-Next setup with a 256K context window, loading all tool definitions at once exhausted the context.
Why Tool Definitions Consume Context
Traditional MCP clients follow a five-step flow: connect to server, fetch tool list via tools/list, register tools locally, provide all definitions to the model, and let the model pick tools by name, description, and schema. The client can keep the full catalog but only send a subset to the model — if it supports Tool Search (lazy loading). DSH's built-in @deepseek-ai/dsh-mcp-client lacks Tool Search, so it dumps every schema into the initial context.
Three Optimization Approaches
The author identifies three non-exclusive strategies:
Tool filtering & per-scene mounting — Expose only tools needed for the current task. Best for clients without Tool Search.
Tool Search / lazy loading — Load full schema only when the model searches for a tool. Best for clients with native support.
Meta-MCP proxy — Hide hundreds of real tools behind two generic endpoints ( search_tools, call_tool). Best when a custom compatibility layer is needed.
1. Reduce Exposed Tool Count
Per-project MCP switching: The author uses REA for reverse-engineering projects and OpenPencil for design projects, loading only the relevant MCP. This cuts tools from ~280 to ~130. Token savings are not linear with tool count because schema sizes vary widely.
Filter by tool family within REA: REA's 130 tools group into families: core-native (open_binary, analyze_function, xrefs), navigation/meta, evidence/reconstruction, compare, android/adb, web/js, debugger (GDB, Frida, Rizin), apple/macho, firmware/pcap, managed/wasm/evm. The author proposes splitting REA into rea-core, rea-debugger, rea-android, rea-web, etc., loading only core by default. A key caveat: client-side tool allow-lists may control execution permission but not schema loading — filtering must happen at the schema-provision layer.
2. MCP Tool Search (Lazy Loading)
Tool Search lets the model query a tool directory first, then fetch full schemas only for selected tools. Example: to open a binary and analyze a function, the model only needs open_binary, analyze_function, xrefs — not all 130 schemas. Trade-offs: extra discovery step, potential search-quality issues, and first-call latency.
DSH ecosystem solutions:
dsh-mcp (GitHub: https://github.com/ArvinQi/dsh-mcp) — adds MCP management UI, per-tool selection, and mcp_tool_search mode. Install via dsh plugin --profile web add dsh-mcp.
dsh-mcp-manager (GitHub: https://github.com/zhang-guo-wen/dsh-mcp-manager) — supports on-demand server startup, tool filtering, search, and pagination.
MCP Lens (GitHub discussion:
https://github.com/deepseek-ai/deepseek-harness/discussions/2137) — uses a unified proxy exposing only mcp_search and mcp_call. Author reports a 1,000-tool test where exposed endpoints dropped from 1,000 to 2, with large schema byte reduction (environment-specific, not directly transferable).
Other editors: Claude Code enables Tool Search by default (controllable via ENABLE_TOOL_SEARCH), Codex auto-enables it, OpenAI documents defer_loading for searchable tool registration.
3. Build a Meta-MCP Proxy
Because DSH updates frequently and breaks plugins, the author implemented a custom Meta-MCP proxy in ~100 lines of Node.js (ESM). It acts as an MCP server to the client and as an MCP client to upstream servers (REA, OpenPencil). Configuration via MCP_UPSTREAM env var (JSON mapping server IDs to command/args). The proxy exposes two tools: search_tools — query tool names, summaries, and optionally full schemas (params: query, server, detail enum [summary|schema], tool, limit). call_tool — forward calls to upstream (params: tool as "server:name", arguments object).
The proxy handles upstream process lifecycle, MCP handshake (protocol version 2025-06-18), tool list caching, keyword search, schema retrieval, call forwarding, timeouts (default 120s via MCP_TIMEOUT), and upstream exit cleanup. The provided meta-mcp.mjs source includes inline comments flagging three known defects: handshake runs only once, tool-name splitting uses first colon, upstream death doesn't fail pending waiters.
#!/usr/bin/env node
// meta-mcp.mjs
// MCP_UPSTREAM='{"rea":{"command":"node","args":["rea.js"]}}' node meta-mcp.mjs
import { spawn } from 'node:child_process';
const CFG = JSON.parse(process.env.MCP_UPSTREAM || '{}');
const RPC_TIMEOUT = Number(process.env.MCP_TIMEOUT || 120000);
const st = new Map(); // id -> {proc,ready,tools,seq,pending,buf}
function conn(id) {
if (st.has(id)) return st.get(id);
const c = CFG[id];
if (!c) throw new Error(`unknown upstream: ${id}`);
const proc = spawn(c.command, c.args || [], { stdio: ['pipe', 'pipe', 'pipe'] });
const s = { proc, ready: null, tools: null, seq: 0, pending: new Map(), buf: '' };
proc.stderr.on('data', d => process.stderr.write(`[${id}] ${d}`));
proc.on('exit', code => { // defect: upstream death should fail waiters
for (const p of s.pending.values()) p.rej(new Error(`upstream ${id} exited (${code})`));
s.pending.clear(); s.ready = null; s.tools = null; st.delete(id);
});
proc.stdout.on('data', d => {
s.buf += d.toString('utf8');
let i;
while ((i = s.buf.indexOf('
')) >= 0) {
const line = s.buf.slice(0, i).trim(); s.buf = s.buf.slice(i + 1);
if (!line) continue;
let m; try { m = JSON.parse(line); } catch { continue; }
const p = s.pending.get(m.id);
if (!p) continue;
s.pending.delete(m.id); clearTimeout(p.t);
m.error ? p.rej(Object.assign(new Error(m.error.message), { code: m.error.code })) : p.res(m.result);
}
});
st.set(id, s);
return s;
}
function rpc(id, method, params) {
const s = conn(id), mid = ++s.seq;
return new Promise((res, rej) => {
const t = setTimeout(() => { s.pending.delete(mid); rej(new Error(`${id}.${method} timeout`)); }, RPC_TIMEOUT);
s.pending.set(mid, { res, rej, t });
s.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: mid, method, params }) + '
');
});
}
// defect: handshake only once
async function ready(id) {
const s = conn(id);
if (!s.ready) {
s.ready = (async () => {
await rpc(id, 'initialize', { protocolVersion: '2025-06-18', capabilities: {},
clientInfo: { name: 'meta-mcp', version: '0.2.0' } });
s.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '
');
})();
s.ready.catch(() => { s.ready = null; });
}
await s.ready;
return s;
}
async function toolsOf(id) {
const s = await ready(id);
if (!s.tools) s.tools = (await rpc(id, 'tools/list', {})).tools || [];
return s.tools;
}
const META = [
{ name: 'search_tools',
description: '检索可用工具,返回 "server:tool — 摘要"。detail="schema" 且给 tool 时返回完整参数 schema。',
inputSchema: { type: 'object', properties: {
query: { type: 'string' }, server: { type: 'string' },
detail: { enum: ['summary', 'schema'] }, tool: { type: 'string' },
limit: { type: 'number', maximum: 200 } } } },
{ name: 'call_tool',
description: '调用真实工具。tool = "rea:xrefs",arguments 为对象。',
inputSchema: { type: 'object', properties: {
tool: { type: 'string' }, arguments: { type: 'object' } }, required: ['tool'] } }
];
async function dispatch(name, a = {}) {
if (name === 'search_tools') {
let rows = [];
for (const id of Object.keys(CFG)) {
for (const t of await toolsOf(id))
rows.push({ key: `${id}:${t.name}`, server: id, name: t.name,
desc: (t.description || '').split('
')[0].slice(0, 110), schema: t.inputSchema });
}
if (a.detail === 'schema') {
const t = rows.find(r => r.key === a.tool || r.name === a.tool);
return { content: [{ type: 'text', text: t ? JSON.stringify({ server: t.server, name: t.name, schema: t.schema }, null, 2) : 'not found' }] };
}
if (a.server) rows = rows.filter(r => r.server === a.server);
if (a.query) { const q = a.query.toLowerCase();
rows = rows.filter(r => r.name.toLowerCase().includes(q) || r.desc.toLowerCase().includes(q)); }
rows = rows.slice(0, a.limit || 40);
return { content: [{ type: 'text', text: rows.map(r => `${r.key}— ${r.desc}`).join('
') || '(no match)' }] };
}
if (name === 'call_tool') {
// defect: split on first ':', tool names containing ':' unaffected
const raw = String(a.tool || ''), k = raw.indexOf(':');
if (k < 0) return { content: [{ type: 'text', text: 'tool must be "server:name"' }], isError: true };
const id = raw.slice(0, k), tname = raw.slice(k + 1);
if (!CFG[id]) return { content: [{ type: 'text', text: `unknown server: ${id}` }], isError: true };
await ready(id);
return await rpc(id, 'tools/call', { name: tname, arguments: a.arguments || {} });
}
return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true };
}
let buf = '';
process.stdin.on('data', async d => {
buf += d.toString('utf8');
let i;
while ((i = buf.indexOf('
')) >= 0) {
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
if (!line) continue;
let req; try { req = JSON.parse(line); } catch { continue; }
if (req.id === undefined) continue;
const send = o => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: req.id, ...o }) + '
');
try {
let result;
if (req.method === 'initialize')
result = { protocolVersion: '2025-06-18', capabilities: { tools: {} },
serverInfo: { name: 'meta-mcp', version: '0.2.0' } };
else if (req.method === 'tools/list')
result = { tools: META };
else if (req.method === 'tools/call')
result = await dispatch(req.params?.name, req.params?.arguments);
else throw Object.assign(new Error(`method not found: ${req.method}`), { code: -32601 });
send({ result });
} catch (e) { send({ error: { code: e.code || -32603, message: String(e.message) } }); }
}
});REA-Specific Schema Optimizations
Beyond client-side fixes, REA's own schemas can shrink:
Evidence ID references: Many tools require a full Evidence object (binary metadata, hashes, paths, analyzer info, config, locations, relationships, environment, confidence, constraints). Replace with {"evidence_id":"ev_..."}; server resolves the stored object.
Two-layer descriptions: Provide short summaries for search phase, full schema + necessary details only when tool is loaded. Avoid truncating descriptions that models need for applicability, parameter relationships, or side-effect awareness.
Split large union types: Some tools use massive oneOf unions for multiple analysis views. Split into separate tools or accept a View ID and let server fetch detailed config. $defs / $ref reduce internal schema duplication but may not cut model context.
Summary
The context explosion came from ~280 tools with heavy schemas. Tool Search reduces initial schema load; per-scene filtering narrows exposure; REA schema optimizations lower per-tool cost; Meta-MCP proxies work when native lazy loading is unavailable or unstable. For most editors, native Tool Search is ideal, but for fast-moving DSH a self-maintained Meta-MCP proxy remains pragmatic until the platform stabilizes.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
DeepHub IMBA
A must‑follow public account sharing practical AI insights. Follow now. internet + machine learning + big data + architecture = IMBA
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
