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:
1 parent
1da304b282
commit
1c14f66356
6 files changed
+657
-650
No files matched your search
+70
-53
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user