perf(daemon): route warm keep-alive calls directly
This commit is contained in:
parent
761c11cb3b
commit
6f063bf585
@ -6,6 +6,7 @@
|
||||
|
||||
- Increase the default OAuth browser wait from 60 seconds to 5 minutes so hosted MCP sign-ins have enough time for account and permission review.
|
||||
- Skip the redundant daemon `status` preflight for warm keep-alive access, cutting one socket round-trip from each routed list/call/resource request while preserving stale-config and dead-daemon recovery.
|
||||
- Route explicit default keep-alive calls like `chrome-devtools.list_pages` through a daemon-only fast path, avoiding full runtime startup on warm calls.
|
||||
|
||||
### Config
|
||||
|
||||
|
||||
258
src/cli.ts
258
src/cli.ts
@ -1,35 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
import { handleAuth, printAuthHelp } from './cli/auth-command.js';
|
||||
import { printCallHelp, handleCall as runHandleCall } from './cli/call-command.js';
|
||||
import { buildGlobalContext } from './cli/cli-factory.js';
|
||||
import { inferCommandRouting } from './cli/command-inference.js';
|
||||
import { handleConfigCli } from './cli/config-command.js';
|
||||
import { handleDaemonCli } from './cli/daemon-command.js';
|
||||
import { handleEmitTs } from './cli/emit-ts-command.js';
|
||||
import { CliUsageError } from './cli/errors.js';
|
||||
import { handleGenerateCli } from './cli/generate-cli-runner.js';
|
||||
import { consumeHelpTokens, isHelpToken, isVersionToken, printHelp, printVersion } from './cli/help-output.js';
|
||||
import { handleInspectCli } from './cli/inspect-cli-command.js';
|
||||
import { handleList, printListHelp } from './cli/list-command.js';
|
||||
import { logError, logInfo } from './cli/logger-context.js';
|
||||
import { handleResource, printResourceHelp } from './cli/resource-command.js';
|
||||
import { DEBUG_HANG, dumpActiveHandles, terminateChildProcesses } from './cli/runtime-debug.js';
|
||||
import { resolveConfigPath } from './config.js';
|
||||
import { DaemonClient } from './daemon/client.js';
|
||||
import { createKeepAliveRuntime } from './daemon/runtime-wrapper.js';
|
||||
import { isKeepAliveServer } from './lifecycle.js';
|
||||
import { createRuntime } from './runtime.js';
|
||||
import type { Runtime, RuntimeOptions } from './runtime.js';
|
||||
|
||||
export { handleAuth, printAuthHelp } from './cli/auth-command.js';
|
||||
export { parseCallArguments } from './cli/call-arguments.js';
|
||||
export { handleCall } from './cli/call-command.js';
|
||||
export { handleGenerateCli } from './cli/generate-cli-runner.js';
|
||||
export { handleInspectCli } from './cli/inspect-cli-command.js';
|
||||
export { extractListFlags, handleList } from './cli/list-command.js';
|
||||
export { handleResource } from './cli/resource-command.js';
|
||||
export { extractListFlags } from './cli/list-flags.js';
|
||||
export { resolveCallTimeout } from './cli/timeouts.js';
|
||||
|
||||
const FORCE_EXIT_GRACE_MS = 50;
|
||||
const DAEMON_FAST_PATH_SERVERS = new Set(['chrome-devtools', 'mobile-mcp', 'playwright']);
|
||||
|
||||
export async function handleAuth(
|
||||
...args: Parameters<typeof import('./cli/auth-command.js').handleAuth>
|
||||
): ReturnType<typeof import('./cli/auth-command.js').handleAuth> {
|
||||
const { handleAuth: imported } = await import('./cli/auth-command.js');
|
||||
return imported(...args);
|
||||
}
|
||||
|
||||
export async function printAuthHelp(): Promise<void> {
|
||||
const { printAuthHelp: imported } = await import('./cli/auth-command.js');
|
||||
imported();
|
||||
}
|
||||
|
||||
export async function handleCall(
|
||||
...args: Parameters<typeof import('./cli/call-command.js').handleCall>
|
||||
): ReturnType<typeof import('./cli/call-command.js').handleCall> {
|
||||
const { handleCall: imported } = await import('./cli/call-command.js');
|
||||
return imported(...args);
|
||||
}
|
||||
|
||||
export async function handleGenerateCli(
|
||||
...args: Parameters<typeof import('./cli/generate-cli-runner.js').handleGenerateCli>
|
||||
): ReturnType<typeof import('./cli/generate-cli-runner.js').handleGenerateCli> {
|
||||
const { handleGenerateCli: imported } = await import('./cli/generate-cli-runner.js');
|
||||
return imported(...args);
|
||||
}
|
||||
|
||||
export async function handleInspectCli(
|
||||
...args: Parameters<typeof import('./cli/inspect-cli-command.js').handleInspectCli>
|
||||
): ReturnType<typeof import('./cli/inspect-cli-command.js').handleInspectCli> {
|
||||
const { handleInspectCli: imported } = await import('./cli/inspect-cli-command.js');
|
||||
return imported(...args);
|
||||
}
|
||||
|
||||
export async function handleList(
|
||||
...args: Parameters<typeof import('./cli/list-command.js').handleList>
|
||||
): ReturnType<typeof import('./cli/list-command.js').handleList> {
|
||||
const { handleList: imported } = await import('./cli/list-command.js');
|
||||
return imported(...args);
|
||||
}
|
||||
|
||||
export async function handleResource(
|
||||
...args: Parameters<typeof import('./cli/resource-command.js').handleResource>
|
||||
): ReturnType<typeof import('./cli/resource-command.js').handleResource> {
|
||||
const { handleResource: imported } = await import('./cli/resource-command.js');
|
||||
return imported(...args);
|
||||
}
|
||||
|
||||
export async function runCli(argv: string[]): Promise<void> {
|
||||
const args = [...argv];
|
||||
@ -66,11 +97,13 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
|
||||
// Early-exit command handlers that don't require runtime inference.
|
||||
if (command === 'generate-cli') {
|
||||
await handleGenerateCli(args, globalFlags);
|
||||
const { handleGenerateCli: importedHandleGenerateCli } = await import('./cli/generate-cli-runner.js');
|
||||
await importedHandleGenerateCli(args, globalFlags);
|
||||
return;
|
||||
}
|
||||
if (command === 'inspect-cli') {
|
||||
await handleInspectCli(args);
|
||||
const { handleInspectCli: importedHandleInspectCli } = await import('./cli/inspect-cli-command.js');
|
||||
await importedHandleInspectCli(args);
|
||||
return;
|
||||
}
|
||||
const rootOverride = globalFlags['--root'];
|
||||
@ -85,6 +118,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
};
|
||||
|
||||
if (command === 'daemon') {
|
||||
const { handleDaemonCli } = await import('./cli/daemon-command.js');
|
||||
await handleDaemonCli(args, {
|
||||
configPath: configPathResolved,
|
||||
configExplicit: configResolution.explicit,
|
||||
@ -94,6 +128,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
if (command === 'config') {
|
||||
const { handleConfigCli } = await import('./cli/config-command.js');
|
||||
await handleConfigCli(
|
||||
{
|
||||
loadOptions: { configPath, rootDir: rootOverride },
|
||||
@ -105,6 +140,10 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
if (command === 'emit-ts') {
|
||||
const [{ createRuntime }, { handleEmitTs }] = await Promise.all([
|
||||
import('./runtime.js'),
|
||||
import('./cli/emit-ts-command.js'),
|
||||
]);
|
||||
const runtime = await createRuntime(runtimeOptionsWithPath);
|
||||
try {
|
||||
await handleEmitTs(runtime, args);
|
||||
@ -114,6 +153,16 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await maybeHandleDaemonFastCall(command, args, configResolution, rootOverride)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ createRuntime }, { DaemonClient }, { createKeepAliveRuntime }, { isKeepAliveServer }] = await Promise.all([
|
||||
import('./runtime.js'),
|
||||
import('./daemon/client.js'),
|
||||
import('./daemon/runtime-wrapper.js'),
|
||||
import('./lifecycle.js'),
|
||||
]);
|
||||
const baseRuntime = await createRuntime(runtimeOptionsWithPath);
|
||||
const keepAliveServers = new Set(
|
||||
baseRuntime
|
||||
@ -142,41 +191,49 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
try {
|
||||
if (resolvedCommand === 'list') {
|
||||
if (consumeHelpTokens(resolvedArgs)) {
|
||||
const { printListHelp } = await import('./cli/list-command.js');
|
||||
printListHelp();
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
await handleList(runtime, resolvedArgs);
|
||||
const { handleList: importedHandleList } = await import('./cli/list-command.js');
|
||||
await importedHandleList(runtime, resolvedArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolvedCommand === 'call') {
|
||||
if (consumeHelpTokens(resolvedArgs)) {
|
||||
const { printCallHelp } = await import('./cli/call-command.js');
|
||||
printCallHelp();
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
const { handleCall: runHandleCall } = await import('./cli/call-command.js');
|
||||
await runHandleCall(runtime, resolvedArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolvedCommand === 'auth') {
|
||||
if (consumeHelpTokens(resolvedArgs)) {
|
||||
printAuthHelp();
|
||||
const { printAuthHelp: importedPrintAuthHelp } = await import('./cli/auth-command.js');
|
||||
importedPrintAuthHelp();
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
await handleAuth(runtime, resolvedArgs);
|
||||
const { handleAuth: importedHandleAuth } = await import('./cli/auth-command.js');
|
||||
await importedHandleAuth(runtime, resolvedArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolvedCommand === 'resource' || resolvedCommand === 'resources') {
|
||||
if (consumeHelpTokens(resolvedArgs)) {
|
||||
const { printResourceHelp } = await import('./cli/resource-command.js');
|
||||
printResourceHelp();
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
await handleResource(runtime, resolvedArgs);
|
||||
const { handleResource: importedHandleResource } = await import('./cli/resource-command.js');
|
||||
await importedHandleResource(runtime, resolvedArgs);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
@ -239,11 +296,158 @@ if (process.env.MCPORTER_DISABLE_AUTORUN !== '1') {
|
||||
});
|
||||
}
|
||||
|
||||
async function invokeAuthCommand(runtimeOptions: Parameters<typeof createRuntime>[0], args: string[]): Promise<void> {
|
||||
async function invokeAuthCommand(runtimeOptions: RuntimeOptions, args: string[]): Promise<void> {
|
||||
const [{ createRuntime }, { handleAuth: importedHandleAuth }] = await Promise.all([
|
||||
import('./runtime.js'),
|
||||
import('./cli/auth-command.js'),
|
||||
]);
|
||||
const runtime = await createRuntime(runtimeOptions);
|
||||
try {
|
||||
await handleAuth(runtime, args);
|
||||
await importedHandleAuth(runtime, args);
|
||||
} finally {
|
||||
await runtime.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeHandleDaemonFastCall(
|
||||
command: string,
|
||||
args: string[],
|
||||
configResolution: { path: string; explicit: boolean },
|
||||
rootDir: string | undefined
|
||||
): Promise<boolean> {
|
||||
const callArgs = resolveDaemonFastCallArgs(command, args);
|
||||
if (!callArgs) {
|
||||
return false;
|
||||
}
|
||||
const server = resolveExplicitCallServer(callArgs);
|
||||
if (!server || !DAEMON_FAST_PATH_SERVERS.has(server) || isFastPathKeepAliveDisabled(server)) {
|
||||
return false;
|
||||
}
|
||||
const [{ DaemonClient }, { handleCall: importedHandleCall }] = await Promise.all([
|
||||
import('./daemon/client.js'),
|
||||
import('./cli/call-command.js'),
|
||||
]);
|
||||
const daemonClient = new DaemonClient({
|
||||
configPath: configResolution.path,
|
||||
configExplicit: configResolution.explicit,
|
||||
rootDir,
|
||||
});
|
||||
await importedHandleCall(createDaemonOnlyRuntime(daemonClient), callArgs);
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveDaemonFastCallArgs(command: string, args: string[]): string[] | undefined {
|
||||
if (command === 'call') {
|
||||
return args;
|
||||
}
|
||||
if (isExplicitNonCallCommand(command) || command.includes('://')) {
|
||||
return undefined;
|
||||
}
|
||||
if (!/[.(]/.test(command)) {
|
||||
return undefined;
|
||||
}
|
||||
return [command, ...args];
|
||||
}
|
||||
|
||||
function isExplicitNonCallCommand(command: string): boolean {
|
||||
return (
|
||||
command === 'list' ||
|
||||
command === 'auth' ||
|
||||
command === 'resource' ||
|
||||
command === 'resources' ||
|
||||
command === 'daemon' ||
|
||||
command === 'config' ||
|
||||
command === 'emit-ts' ||
|
||||
command === 'generate-cli' ||
|
||||
command === 'inspect-cli' ||
|
||||
command === 'describe'
|
||||
);
|
||||
}
|
||||
|
||||
function resolveExplicitCallServer(args: readonly string[]): string | undefined {
|
||||
let serverFlag: string | undefined;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const token = args[index];
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (isHelpToken(token)) {
|
||||
return undefined;
|
||||
}
|
||||
if (token === '--http-url' || token === '--stdio') {
|
||||
return undefined;
|
||||
}
|
||||
if (token === '--server') {
|
||||
serverFlag = args[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('--server=')) {
|
||||
serverFlag = token.slice('--server='.length);
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith('-')) {
|
||||
continue;
|
||||
}
|
||||
if (token.includes('://')) {
|
||||
return undefined;
|
||||
}
|
||||
const separator = token.indexOf('.');
|
||||
if (separator > 0) {
|
||||
return token.slice(0, separator);
|
||||
}
|
||||
return serverFlag;
|
||||
}
|
||||
return serverFlag;
|
||||
}
|
||||
|
||||
function isFastPathKeepAliveDisabled(server: string): boolean {
|
||||
const raw = process.env.MCPORTER_DISABLE_KEEPALIVE ?? process.env.MCPORTER_NO_KEEPALIVE;
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const disabled = new Set(
|
||||
raw
|
||||
.split(',')
|
||||
.map((entry) => entry.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
return disabled.has('*') || disabled.has(server.toLowerCase());
|
||||
}
|
||||
|
||||
function createDaemonOnlyRuntime(daemonClient: import('./daemon/client.js').DaemonClient): Runtime {
|
||||
return {
|
||||
listServers: () => [],
|
||||
getDefinitions: () => [],
|
||||
getDefinition: (server: string) => {
|
||||
throw new Error(`Server '${server}' is only available through the keep-alive daemon fast path.`);
|
||||
},
|
||||
registerDefinition: () => {
|
||||
throw new Error('Ad-hoc servers are not supported by the keep-alive daemon fast path.');
|
||||
},
|
||||
getInstructions: async () => undefined,
|
||||
listTools: async (server, options) =>
|
||||
(await daemonClient.listTools({
|
||||
server,
|
||||
includeSchema: options?.includeSchema,
|
||||
autoAuthorize: options?.autoAuthorize,
|
||||
})) as Awaited<ReturnType<Runtime['listTools']>>,
|
||||
callTool: (server, toolName, options) =>
|
||||
daemonClient.callTool({
|
||||
server,
|
||||
tool: toolName,
|
||||
args: options?.args,
|
||||
timeoutMs: options?.timeoutMs,
|
||||
}),
|
||||
listResources: (server, options) => daemonClient.listResources({ server, params: options ?? {} }),
|
||||
readResource: (server, uri) => daemonClient.readResource({ server, uri }),
|
||||
connect: async (server) => {
|
||||
throw new Error(`Server '${server}' is only available through daemon request methods.`);
|
||||
},
|
||||
close: async (server?: string) => {
|
||||
if (server) {
|
||||
await daemonClient.closeServer({ server }).catch(() => {});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -21,7 +21,6 @@ import {
|
||||
} from './identifier-helpers.js';
|
||||
import { saveCallImagesIfRequested } from './image-output.js';
|
||||
import { buildConnectionIssueEnvelope } from './json-output.js';
|
||||
import { handleList } from './list-command.js';
|
||||
import type { OutputFormat } from './output-utils.js';
|
||||
import { printCallOutput, tailLogIfRequested } from './output-utils.js';
|
||||
import { dumpActiveHandles } from './runtime-debug.js';
|
||||
@ -232,6 +231,7 @@ async function maybeDescribeServer(
|
||||
if (outputFormat === 'json') {
|
||||
listArgs.push('--json');
|
||||
}
|
||||
const { handleList } = await import('./list-command.js');
|
||||
await handleList(runtime, listArgs);
|
||||
return true;
|
||||
}
|
||||
@ -251,6 +251,7 @@ async function maybeDescribeServer(
|
||||
if (outputFormat === 'json') {
|
||||
listArgs.push('--json');
|
||||
}
|
||||
const { handleList } = await import('./list-command.js');
|
||||
await handleList(runtime, listArgs);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2,14 +2,13 @@ import ora from 'ora';
|
||||
import type { ServerDefinition } from '../config.js';
|
||||
import { MCPORTER_VERSION } from '../runtime.js';
|
||||
import { setStdioLogMode } from '../sdk-patches.js';
|
||||
import type { EphemeralServerSpec } from './adhoc-server.js';
|
||||
import { extractEphemeralServerFlags } from './ephemeral-flags.js';
|
||||
import { persistPreparedEphemeralServer, prepareEphemeralServerTarget } from './ephemeral-target.js';
|
||||
import { splitHttpToolSelector } from './http-utils.js';
|
||||
import { chooseClosestIdentifier, renderIdentifierResolutionMessages } from './identifier-helpers.js';
|
||||
import { formatExampleBlock } from './list-detail-helpers.js';
|
||||
import type { ListSummaryResult, StatusCategory } from './list-format.js';
|
||||
import { classifyListError, formatSourceSuffix, renderServerListRow } from './list-format.js';
|
||||
import { extractListFlags } from './list-flags.js';
|
||||
import type { ToolMetadata } from './generate/tools.js';
|
||||
import {
|
||||
buildAuthCommandHint,
|
||||
@ -22,96 +21,11 @@ import {
|
||||
printToolDetail,
|
||||
summarizeStatusCounts,
|
||||
} from './list-output.js';
|
||||
import { consumeOutputFormat } from './output-format.js';
|
||||
import { dimText, extraDimText, supportsSpinner, yellowText } from './terminal.js';
|
||||
import { consumeTimeoutFlag, LIST_TIMEOUT_MS, withTimeout } from './timeouts.js';
|
||||
import { LIST_TIMEOUT_MS, withTimeout } from './timeouts.js';
|
||||
import { loadToolMetadata } from './tool-cache.js';
|
||||
import { formatTransportSummary } from './transport-utils.js';
|
||||
|
||||
export function extractListFlags(args: string[]): {
|
||||
schema: boolean;
|
||||
timeoutMs?: number;
|
||||
requiredOnly: boolean;
|
||||
ephemeral?: EphemeralServerSpec;
|
||||
format: ListOutputFormat;
|
||||
verbose: boolean;
|
||||
includeSources: boolean;
|
||||
brief: boolean;
|
||||
} {
|
||||
let schema = false;
|
||||
let timeoutMs: number | undefined;
|
||||
let requiredOnly = true;
|
||||
let verbose = false;
|
||||
let includeSources = false;
|
||||
let brief = false;
|
||||
const format = consumeOutputFormat(args, {
|
||||
defaultFormat: 'text',
|
||||
allowed: ['text', 'json'],
|
||||
enableRawShortcut: false,
|
||||
jsonShortcutFlag: '--json',
|
||||
}) as ListOutputFormat;
|
||||
const ephemeral = extractEphemeralServerFlags(args);
|
||||
let index = 0;
|
||||
while (index < args.length) {
|
||||
const token = args[index];
|
||||
if (token === '--schema') {
|
||||
schema = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--yes') {
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--all-parameters') {
|
||||
requiredOnly = false;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--verbose') {
|
||||
verbose = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--sources') {
|
||||
includeSources = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--brief' || token === '--signatures') {
|
||||
brief = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--timeout') {
|
||||
timeoutMs = consumeTimeoutFlag(args, index, { flagName: '--timeout' });
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if (brief) {
|
||||
const conflicts: string[] = [];
|
||||
if (format === 'json') {
|
||||
conflicts.push('--json');
|
||||
}
|
||||
if (schema) {
|
||||
conflicts.push('--schema');
|
||||
}
|
||||
if (verbose) {
|
||||
conflicts.push('--verbose');
|
||||
}
|
||||
if (!requiredOnly) {
|
||||
conflicts.push('--all-parameters');
|
||||
}
|
||||
if (conflicts.length > 0) {
|
||||
throw new Error(`--brief cannot be used with ${conflicts.join(', ')}`);
|
||||
}
|
||||
}
|
||||
return { schema, timeoutMs, requiredOnly, ephemeral, format, verbose, includeSources, brief };
|
||||
}
|
||||
|
||||
type ListOutputFormat = 'text' | 'json';
|
||||
|
||||
export async function handleList(
|
||||
runtime: Awaited<ReturnType<(typeof import('../runtime.js'))['createRuntime']>>,
|
||||
args: string[]
|
||||
|
||||
88
src/cli/list-flags.ts
Normal file
88
src/cli/list-flags.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { EphemeralServerSpec } from './adhoc-server.js';
|
||||
import { extractEphemeralServerFlags } from './ephemeral-flags.js';
|
||||
import { consumeOutputFormat } from './output-format.js';
|
||||
import { consumeTimeoutFlag } from './timeouts.js';
|
||||
|
||||
export type ListOutputFormat = 'text' | 'json';
|
||||
|
||||
export function extractListFlags(args: string[]): {
|
||||
schema: boolean;
|
||||
timeoutMs?: number;
|
||||
requiredOnly: boolean;
|
||||
ephemeral?: EphemeralServerSpec;
|
||||
format: ListOutputFormat;
|
||||
verbose: boolean;
|
||||
includeSources: boolean;
|
||||
brief: boolean;
|
||||
} {
|
||||
let schema = false;
|
||||
let timeoutMs: number | undefined;
|
||||
let requiredOnly = true;
|
||||
let verbose = false;
|
||||
let includeSources = false;
|
||||
let brief = false;
|
||||
const format = consumeOutputFormat(args, {
|
||||
defaultFormat: 'text',
|
||||
allowed: ['text', 'json'],
|
||||
enableRawShortcut: false,
|
||||
jsonShortcutFlag: '--json',
|
||||
}) as ListOutputFormat;
|
||||
const ephemeral = extractEphemeralServerFlags(args);
|
||||
let index = 0;
|
||||
while (index < args.length) {
|
||||
const token = args[index];
|
||||
if (token === '--schema') {
|
||||
schema = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--yes') {
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--all-parameters') {
|
||||
requiredOnly = false;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--verbose') {
|
||||
verbose = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--sources') {
|
||||
includeSources = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--brief' || token === '--signatures') {
|
||||
brief = true;
|
||||
args.splice(index, 1);
|
||||
continue;
|
||||
}
|
||||
if (token === '--timeout') {
|
||||
timeoutMs = consumeTimeoutFlag(args, index, { flagName: '--timeout' });
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if (brief) {
|
||||
const conflicts: string[] = [];
|
||||
if (format === 'json') {
|
||||
conflicts.push('--json');
|
||||
}
|
||||
if (schema) {
|
||||
conflicts.push('--schema');
|
||||
}
|
||||
if (verbose) {
|
||||
conflicts.push('--verbose');
|
||||
}
|
||||
if (!requiredOnly) {
|
||||
conflicts.push('--all-parameters');
|
||||
}
|
||||
if (conflicts.length > 0) {
|
||||
throw new Error(`--brief cannot be used with ${conflicts.join(', ')}`);
|
||||
}
|
||||
}
|
||||
return { schema, timeoutMs, requiredOnly, ephemeral, format, verbose, includeSources, brief };
|
||||
}
|
||||
@ -2,7 +2,7 @@ import crypto, { randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
import { listConfigLayerPaths } from '../config.js';
|
||||
import { listConfigLayerPaths } from '../config/path-discovery.js';
|
||||
import { launchDaemonDetached } from './launch.js';
|
||||
import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js';
|
||||
import type {
|
||||
|
||||
84
tests/cli-daemon-fast-path.test.ts
Normal file
84
tests/cli-daemon-fast-path.test.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
process.env.MCPORTER_DISABLE_AUTORUN = '1';
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const daemonCallTool = vi.fn();
|
||||
const daemonListTools = vi.fn();
|
||||
const daemonCloseServer = vi.fn();
|
||||
const DaemonClient = vi.fn(function MockDaemonClient() {
|
||||
return {
|
||||
callTool: daemonCallTool,
|
||||
listTools: daemonListTools,
|
||||
listResources: vi.fn(),
|
||||
readResource: vi.fn(),
|
||||
closeServer: daemonCloseServer,
|
||||
};
|
||||
});
|
||||
return {
|
||||
DaemonClient,
|
||||
createRuntime: vi.fn(),
|
||||
daemonCallTool,
|
||||
daemonListTools,
|
||||
daemonCloseServer,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../src/daemon/client.js', () => ({
|
||||
DaemonClient: mocks.DaemonClient,
|
||||
}));
|
||||
|
||||
vi.mock('../src/runtime.js', () => ({
|
||||
MCPORTER_VERSION: 'test',
|
||||
createRuntime: mocks.createRuntime,
|
||||
}));
|
||||
|
||||
describe('daemon call fast path', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
mocks.DaemonClient.mockClear();
|
||||
mocks.createRuntime.mockClear();
|
||||
mocks.daemonCallTool.mockReset().mockResolvedValue({
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
});
|
||||
mocks.daemonListTools.mockReset().mockResolvedValue([]);
|
||||
mocks.daemonCloseServer.mockReset().mockResolvedValue(undefined);
|
||||
process.exitCode = undefined;
|
||||
});
|
||||
|
||||
it('routes explicit default keep-alive calls without building the full runtime', async () => {
|
||||
const { runCli } = await import('../src/cli.js');
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await runCli(['call', 'chrome-devtools.list_pages', '--output', 'json']);
|
||||
|
||||
expect(mocks.createRuntime).not.toHaveBeenCalled();
|
||||
expect(mocks.DaemonClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
configExplicit: false,
|
||||
})
|
||||
);
|
||||
expect(mocks.daemonCallTool).toHaveBeenCalledWith({
|
||||
server: 'chrome-devtools',
|
||||
tool: 'list_pages',
|
||||
args: {},
|
||||
timeoutMs: expect.any(Number),
|
||||
});
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"text": "ok"'));
|
||||
});
|
||||
|
||||
it('also routes inferred call tokens through the daemon fast path', async () => {
|
||||
const { runCli } = await import('../src/cli.js');
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await runCli(['chrome-devtools.list_pages', '--output', 'json']);
|
||||
|
||||
expect(mocks.createRuntime).not.toHaveBeenCalled();
|
||||
expect(mocks.daemonCallTool).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
server: 'chrome-devtools',
|
||||
tool: 'list_pages',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user