feat(compression): wire MCP tool-cardinality reduction (F4.3, opt-in) (#4221)

Integrated into release/v3.8.29 (F4.3 opt-in MCP tool-cardinality reduction; baseline rebaselined server.ts 1458->1468).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-18 22:03:08 -03:00
committed by GitHub
parent 2089e5cce9
commit eef9d203fb
4 changed files with 79 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
{
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_06_18_4221_tool_cardinality": "PR #4221 own growth: server.ts 1458->1468 (+10 at the existing registerTool override in createMcpServer = F4.3 opt-in tool-cardinality wiring). Reads readMcpToolProfileFromEnv(process.env) once (MCP_TOOL_DENY/MCP_TOOL_ALLOW; null = no filter, the default), and when a registered tool is denied by the profile calls registered.disable() so it is not announced in tools/list (token savings). The default (null) profile never enters the branch — existing behavior byte-identical. The reusable parser + reduceToolManifest decision live in the (non-frozen) toolCardinality.ts. Cohesive opt-in feature at the registration chokepoint; not extractable without hiding the register boundary.",
"_rebaseline_2026_06_18_4217_compression_step_streaming": "PR #4217 own growth: chatCore.ts 5063->5086 (+23 at the existing compression-apply chokepoint = the best-effort onEngineStep callback threaded into applyCompressionAsync). The callback builds a compression.step payload and fires emit(\"compression.step\", …) + forwardDashboardEventToLiveWs(…) once per stacked engine as it completes (F3.3 live per-engine streaming), wrapped in try/catch so it never fails the request. It closes over the same emit/traceId/mode locals as the compression.completed emit right below it (line 1749); the reusable per-engine emission lives in strategySelector.ts (reportEngineStep + StackedCompressionStep) and the studio reducers in compressionFlowModel.ts (both <cap). Not extractable without hiding the emit boundary, mirroring the prior compression rebaselines (#4210/#4004). Structural shrink of chatCore.ts tracked in #3501.",
"_rebaseline_2026_06_18_4210_engine_breakdown": "PR #4210 own growth: chatCore.ts 5060->5063 (+3 = wire ensureEngineBreakdown(result.stats) into the existing compression.completed emit + its import line). Single-engine modes (rtk/lite/standard/aggressive/ultra) leave stats.engineBreakdown empty, which made the dashboard studio render an empty Input->Output pipeline (no engine node); the synthesized 1-entry breakdown lives in the new pure leaf open-sse/services/compression/engineBreakdown.ts (<cap), mirroring seedLatestCompressionRunFromDb. The +3 is the import + a 2-line explanatory comment at the emit chokepoint; not extractable further without hiding the emit boundary.",
"_rebaseline_2026_06_18_4202_zenmux_live_models": "Issue #4202 own growth: providers/[id]/models/route.ts 2531->2534 (+3 = one NAMED_OPENAI_STYLE_PROVIDERS Set entry `zenmux` + a 2-line comment). zenmux carries a real modelsUrl but was not classified by any live-fetch branch, so its hardcoded 9-entry registry catalog was served (source local_catalog, 'API unavailable — using local catalog') instead of the upstream list — hiding the free models it advertises (z-ai/glm-5.2-free, moonshotai/kimi-k2.7-code-free). Same fix shape as #3976 (llm7/byteplus): the `<baseUrl>/models` probe (after stripping /chat/completions) resolves to https://zenmux.ai/api/v1/models. Pure additive Set membership; not extractable.",
@@ -76,7 +77,7 @@
"open-sse/handlers/sseParser.ts": 812,
"open-sse/handlers/videoGeneration.ts": 1078,
"open-sse/mcp-server/schemas/tools.ts": 1437,
"open-sse/mcp-server/server.ts": 1458,
"open-sse/mcp-server/server.ts": 1468,
"open-sse/mcp-server/tools/advancedTools.ts": 1118,
"open-sse/services/accountFallback.ts": 1727,
"open-sse/services/batchProcessor.ts": 828,

View File

@@ -85,6 +85,7 @@ import { gamificationTools } from "./tools/gamificationTools.ts";
import { notionTools } from "./tools/notionTools.ts";
import { obsidianTools } from "./tools/obsidianTools.ts";
import { compressMcpRegistryMetadata } from "./descriptionCompressor.ts";
import { reduceToolManifest, readMcpToolProfileFromEnv } from "./toolCardinality.ts";
import { smartFilterText } from "../services/compression/engines/mcpAccessibility/index.ts";
import {
DEFAULT_MCP_ACCESSIBILITY_CONFIG,
@@ -797,6 +798,8 @@ export function createMcpServer(): McpServer {
});
const mcpDescriptionCompressionEnabled = readMcpDescriptionCompressionEnabled();
const mcpAccessibilityConfig = readMcpAccessibilityConfig();
// F4.3 tool-cardinality: opt-in tool profile (MCP_TOOL_DENY / MCP_TOOL_ALLOW). null = no filter.
const toolProfile = readMcpToolProfileFromEnv(process.env);
const registerTool = server.registerTool.bind(server);
server.registerTool = ((name: string, config: Record<string, unknown>, handler: unknown) => {
const metadata = compressMcpRegistryMetadata(config, {
@@ -818,7 +821,14 @@ export function createMcpServer(): McpServer {
return result;
}
: handler;
return registerTool(name, metadata, filteredHandler as never);
const registered = registerTool(name, metadata, filteredHandler as never);
if (toolProfile && reduceToolManifest([{ name, scopes: [] }], toolProfile).length === 0) {
// Denied by the cardinality profile: keep the registration valid but disable it so the tool
// is not announced in tools/list (token savings). The default profile never reaches here.
const disablable = registered as unknown as { disable?: () => void };
if (typeof disablable?.disable === "function") disablable.disable();
}
return registered;
}) as typeof server.registerTool;
const registerPrompt = server.registerPrompt.bind(server);
server.registerPrompt = ((name: string, config: Record<string, unknown>, handler: unknown) => {

View File

@@ -222,3 +222,33 @@ export function estimateManifestTokens(
return sum + nameTokens + descTokens;
}, 0);
}
/**
* Build an opt-in {@link ToolProfile} from environment variables, or `null` when unset (no
* filtering — the default). Used by the MCP server to disable tools it should not announce.
*
* MCP_TOOL_DENY — comma-separated tool names to always drop
* MCP_TOOL_ALLOW — comma-separated tool names to keep exclusively (allow-list mode)
*
* Scope-based (`allowScopes`) and `maxTools` filtering need the full manifest at registration
* and are intentionally not env-exposed here (a tools/list-level hook is a tracked follow-up).
*/
export function readMcpToolProfileFromEnv(
env: Record<string, string | undefined>
): ToolProfile | null {
const parse = (value: string | undefined): string[] =>
value
? value
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
const denyTools = parse(env["MCP_TOOL_DENY"]);
const allowTools = parse(env["MCP_TOOL_ALLOW"]);
if (denyTools.length === 0 && allowTools.length === 0) return null;
return {
name: "env",
...(denyTools.length > 0 ? { denyTools } : {}),
...(allowTools.length > 0 ? { allowTools } : {}),
};
}

View File

@@ -0,0 +1,36 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
readMcpToolProfileFromEnv,
reduceToolManifest,
} from "../../../open-sse/mcp-server/toolCardinality.ts";
// F4.3 wiring: the MCP server now consults an opt-in tool profile (MCP_TOOL_DENY / MCP_TOOL_ALLOW)
// and disables denied tools so they are not announced to the model (token savings). Default (no
// env) is a no-op — every tool stays registered.
describe("MCP tool profile from env (cardinality opt-in)", () => {
it("returns null when no deny/allow env is set (no-op)", () => {
assert.equal(readMcpToolProfileFromEnv({}), null);
assert.equal(readMcpToolProfileFromEnv({ MCP_TOOL_DENY: "", MCP_TOOL_ALLOW: " " }), null);
});
it("parses MCP_TOOL_DENY / MCP_TOOL_ALLOW into a profile (trimmed, empties dropped)", () => {
const p = readMcpToolProfileFromEnv({ MCP_TOOL_DENY: "a, b ,c", MCP_TOOL_ALLOW: "x ," });
assert.deepEqual(p?.denyTools, ["a", "b", "c"]);
assert.deepEqual(p?.allowTools, ["x"]);
});
it("the deny gate drops a denied tool and keeps others (single-entry manifest decision)", () => {
const profile = readMcpToolProfileFromEnv({ MCP_TOOL_DENY: "noisy_tool" });
assert.ok(profile);
assert.equal(reduceToolManifest([{ name: "noisy_tool", scopes: [] }], profile).length, 0);
assert.equal(reduceToolManifest([{ name: "useful_tool", scopes: [] }], profile).length, 1);
});
it("allow-list mode keeps only the listed tools", () => {
const profile = readMcpToolProfileFromEnv({ MCP_TOOL_ALLOW: "keep_me" });
assert.ok(profile);
assert.equal(reduceToolManifest([{ name: "keep_me", scopes: [] }], profile).length, 1);
assert.equal(reduceToolManifest([{ name: "other", scopes: [] }], profile).length, 0);
});
});