mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
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:
committed by
GitHub
parent
2089e5cce9
commit
eef9d203fb
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user