perf(daemon): route warm keep-alive calls directly

This commit is contained in:
Peter Steinberger 2026-05-09 08:31:16 +01:00
parent 761c11cb3b
commit 6f063bf585
No known key found for this signature in database
7 changed files with 409 additions and 117 deletions

View File

@ -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

View File

@ -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(() => {});
}
},
};
}

View File

@ -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;
}

View File

@ -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
View 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 };
}

View File

@ -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 {

View 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',
})
);
});
});