refactor(contracts): register CLI and MCP tools by family id (#8051)

* refactor(contracts): register CLI and MCP tools by family id

Walk the published tool-family list once for each adapter so a new
family id must supply both a CLI registrar and an MCP registrar.

* fix(cli): keep family registration off the contract tables

Walk family ids from the startup-safe list, and register the web script
commands with the application family that owns their MCP contracts.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-10-30 04:25:48 +08:00
1 parent 1da304b282
commit 1c14f66356
6 files changed
+657 -650

No files matched your search

+70 -53
View File
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { writeAmpRegistrationBackup } from "./cli/ampRegistrationBackup.js";
import { registerToolFamilies } from "./contracts/toolFamilyIds.js";
import { registerGpuCommands } from "./cli/gpuCommands.js";
import { registerNativeFunctionMatchingCommands } from "./cli/nativeFunctionMatchingCommands.js";
import { registerCaptureCommands } from "./cli/captureCommands.js";
@@ -170,59 +171,75 @@ export const createCli = (
const cli = timedCli as unknown as CliInstance;
registerSetupCommands(cli, logger, environment);
registerGuidanceCommands(cli, logger);
registerGuidedWorkflowCommands(cli, logger);
registerReverseEngineeringCommands(cli, logger, environment);
registerCutterCommands(cli, logger, environment);
registerCoreAnalysisCommands(
cli,
logger,
analysis.runDirectAnalysis,
environment,
);
registerUtilityCommands(cli, logger, environment, analysis);
registerWasmCommands(cli, logger, environment);
registerApkSignatureCommands(cli, logger, environment);
registerCaptureCommands(cli, logger, environment);
registerSignalCommands(cli, logger, environment);
registerEvmCommands(cli, logger, environment);
registerDesktopApplicationCommands(cli, logger);
registerGoCommands(cli, logger);
registerIl2cppCommands(cli, logger);
registerModelCommands(cli, logger);
registerGpuCommands(cli, logger);
registerRustCommands(cli, logger);
registerPythonPycCommands(cli, logger);
registerErlangCommands(cli, logger);
registerSqliteCommands(cli, logger, environment);
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
registerManagedCommands(cli, logger, analysis.runProviderAnalysis);
registerAndroidCommands(cli, logger, environment);
registerApktoolCommands(cli, logger, environment);
registerPcapCommands(cli, logger, environment);
registerFlutterCommands(cli, logger);
registerJebCommands(cli, logger, environment);
registerAdbCommands(cli, logger, environment);
registerFirmwareCommands(cli, logger, environment);
registerBinaryDiagnosticsCommands(cli, logger, environment);
registerAnalysisViewCommands(cli, logger);
registerNativeFunctionMatchingCommands(cli, logger, environment);
registerFridaCommands(cli, logger, fridaInstrumentation);
registerEvidenceCommands(cli, logger);
registerEnvironmentCommands(cli, logger, environment);
registerProcessCommands(cli, logger, environment);
registerBrowserCommands(cli, logger);
registerWebScriptCommands(cli, logger);
registerWebModuleCommands(cli, logger, environment);
registerWebSourceLocationCommands(cli, logger, environment);
registerWebRuntimeCommands(cli, logger);
registerWebNetworkCaptureCommands(cli, logger, environment);
registerJavaScriptRecoveryCommands(cli, logger, environment);
registerAdvancedBrowserCommands(cli, logger);
registerBrowserScenarioCommands(cli, logger, environment);
registerElectronCommands(cli, logger, environment);
registerJavaScriptRuntimeObservationCommands(cli, logger, environment);
registerApplicationCommands(cli, logger);
registerToolFamilies({
direct: () =>
registerCoreAnalysisCommands(
cli,
logger,
analysis.runDirectAnalysis,
environment,
),
// Enhanced workflows are MCP tools. Their CLI entry points live with core analysis.
enhanced: () => undefined,
native: () => {
registerReverseEngineeringCommands(cli, logger, environment);
registerCutterCommands(cli, logger, environment);
registerUtilityCommands(cli, logger, environment, analysis);
registerBinaryDiagnosticsCommands(cli, logger, environment);
},
artifact: () => {
registerWasmCommands(cli, logger, environment);
registerApkSignatureCommands(cli, logger, environment);
registerCaptureCommands(cli, logger, environment);
registerSignalCommands(cli, logger, environment);
registerEvmCommands(cli, logger, environment);
registerDesktopApplicationCommands(cli, logger);
registerGoCommands(cli, logger);
registerIl2cppCommands(cli, logger);
registerModelCommands(cli, logger);
registerGpuCommands(cli, logger);
registerRustCommands(cli, logger);
registerPythonPycCommands(cli, logger);
registerErlangCommands(cli, logger);
registerSqliteCommands(cli, logger, environment);
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
},
managed: () =>
registerManagedCommands(cli, logger, analysis.runProviderAnalysis),
firmware: () => registerFirmwareCommands(cli, logger, environment),
android: () => registerAndroidCommands(cli, logger, environment),
pcap: () => registerPcapCommands(cli, logger, environment),
flutter: () => registerFlutterCommands(cli, logger),
frida: () => registerFridaCommands(cli, logger, fridaInstrumentation),
apktool: () => registerApktoolCommands(cli, logger, environment),
jeb: () => registerJebCommands(cli, logger, environment),
adb: () => registerAdbCommands(cli, logger, environment),
browser: () => {
registerBrowserCommands(cli, logger);
registerWebRuntimeCommands(cli, logger);
registerWebNetworkCaptureCommands(cli, logger, environment);
registerAdvancedBrowserCommands(cli, logger);
registerBrowserScenarioCommands(cli, logger, environment);
},
electron: () => registerElectronCommands(cli, logger, environment),
"javascript-runtime": () =>
registerJavaScriptRuntimeObservationCommands(cli, logger, environment),
application: () => {
registerGuidanceCommands(cli, logger);
registerGuidedWorkflowCommands(cli, logger);
registerAnalysisViewCommands(cli, logger);
registerEvidenceCommands(cli, logger);
registerEnvironmentCommands(cli, logger, environment);
registerProcessCommands(cli, logger, environment);
registerJavaScriptRecoveryCommands(cli, logger, environment);
registerWebScriptCommands(cli, logger);
registerWebModuleCommands(cli, logger, environment);
registerWebSourceLocationCommands(cli, logger, environment);
registerApplicationCommands(cli, logger);
},
session: () =>
registerNativeFunctionMatchingCommands(cli, logger, environment),
});
presentOmittableDefaults(cli);
if (resultOutput !== undefined) streamCliCommandResults(cli, resultOutput);
const serveStrict = rejectSurplusCliArguments(cli);
+19 -1
View File
@@ -5,7 +5,11 @@ import {
contractsForToolFamilies,
toolFamilyId,
} from "./toolFamilies.js";
import { TOOL_FAMILY_IDS } from "./toolFamilyIds.js";
import {
registerToolFamilies,
TOOL_FAMILY_IDS,
type ToolFamilyId,
} from "./toolFamilyIds.js";
import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js";
import type {
officialOutputSchemas,
@@ -43,6 +47,20 @@ describe("tool families", () => {
]);
});
it("runs registrars in published id order", () => {
const seen: string[] = [];
const registrars = Object.fromEntries(
TOOL_FAMILY_IDS.map((id) => [
id,
(): void => {
seen.push(id);
},
]),
) as unknown as { readonly [Id in ToolFamilyId]: () => void };
registerToolFamilies(registrars);
expect(seen).toEqual([...TOOL_FAMILY_IDS]);
});
it("covers every contract exactly once", () => {
const owners = new Map<string, string[]>();
for (const { id, contracts } of TOOL_FAMILY_DEFINITIONS)
+1 -1
View File
@@ -44,7 +44,7 @@ import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js";
import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js";
import { FRIDA_TOOL_CONTRACTS } from "./fridaToolContracts.js";
import { TOOL_CONTRACTS } from "./toolContracts.js";
import { TOOL_FAMILY_IDS, type ToolFamilyId } from "./toolFamilyIds.js";
import type { ToolFamilyId } from "./toolFamilyIds.js";
/** One selectable family: a stable selection id and its complete contract set. */
export interface ToolFamilyDefinition {
+12
View File
@@ -31,3 +31,15 @@ export type ToolFamilyId = (typeof TOOL_FAMILY_IDS)[number];
/** Whether a parsed selection value names a published tool family. */
export const isToolFamilyId = (value: string): value is ToolFamilyId =>
(TOOL_FAMILY_IDS as readonly string[]).includes(value);
/**
* Invoke one registrar per published family, in {@link TOOL_FAMILY_IDS} order.
* Both CLI and MCP pass a complete map; a new id fails those call sites.
* This module stays free of contract schemas so CLI startup can walk the list
* without loading every tool definition.
*/
export const registerToolFamilies = (registrars: {
readonly [Id in ToolFamilyId]: () => void;
}): void => {
for (const id of TOOL_FAMILY_IDS) registrars[id]();
};
+491 -595
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,64 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { toolFamilyId } from "../../../src/contracts/toolFamilies.js";
const SHARED_APPLICATION_TOOLS = [
["export_web_scripts", "registerWebScriptCommands", "registerWebScriptTool"],
[
"trace_web_module_imports",
"registerWebModuleCommands",
"registerWebModuleTool",
],
[
"trace_web_source_location",
"registerWebSourceLocationCommands",
"registerWebSourceLocationTool",
],
] as const;
const familyBody = (source: string, family: string): string => {
const marker = `${family}: () =>`;
const start = source.indexOf(marker);
const open = source.indexOf("{", start);
let depth = 0;
for (let index = open; index < source.length; index += 1) {
const character = source[index];
if (character === "{") depth += 1;
else if (character === "}") {
depth -= 1;
if (depth === 0) return source.slice(open, index + 1);
}
}
throw new Error(`unclosed ${family} registrar`);
};
describe("CLI and MCP tool family ownership", () => {
const cli = readFileSync("src/cli.ts", "utf8");
const server = readFileSync("src/server/createServer.ts", "utf8");
it("walks family ids without loading the contract tables", () => {
expect(cli).toContain('from "./contracts/toolFamilyIds.js"');
expect(cli).not.toContain("toolFamilies.js");
expect(server).toContain('from "../contracts/toolFamilyIds.js"');
expect(server).not.toContain("toolFamilies.js");
});
it("keeps web script commands in the application family on both adapters", () => {
const cliApplication = familyBody(cli, "application");
const cliBrowser = familyBody(cli, "browser");
const serverApplication = familyBody(server, "application");
const serverBrowser = familyBody(server, "browser");
for (const [
tool,
cliRegistrar,
serverRegistrar,
] of SHARED_APPLICATION_TOOLS) {
expect(toolFamilyId(tool)).toBe("application");
expect(cliApplication).toContain(cliRegistrar);
expect(cliBrowser).not.toContain(cliRegistrar);
expect(serverApplication).toContain(serverRegistrar);
expect(serverBrowser).not.toContain(serverRegistrar);
}
});
});