From e6b451aee313cf5cad381cbf7949c4e400f99dd2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 11:54:05 +0100 Subject: [PATCH] perf: speed up daemon fast-path calls --- CHANGELOG.md | 1 + src/cli.ts | 54 +++++++++++++++++++++++- src/cli/cli-factory.ts | 2 +- src/cli/config/doctor.ts | 2 +- src/cli/generate/artifacts.ts | 3 +- src/cli/generate/template.ts | 2 +- src/cli/help-output.ts | 2 +- src/cli/list-command.ts | 2 +- src/daemon/client.ts | 15 ++++--- src/runtime.ts | 16 ++----- src/version.ts | 9 ++++ tests/daemon-client-config-stale.test.ts | 3 +- 12 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 src/version.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cc45438..c27a68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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. +- Further reduce warm keep-alive call startup by avoiding runtime/config schema imports on CLI boot and using a narrower daemon call path for simple explicit calls. ### Config diff --git a/src/cli.ts b/src/cli.ts index 43d0a68..02c8644 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,7 +5,7 @@ import { CliUsageError } from './cli/errors.js'; import { consumeHelpTokens, isHelpToken, isVersionToken, printHelp, printVersion } from './cli/help-output.js'; import { logError, logInfo } from './cli/logger-context.js'; import { DEBUG_HANG, dumpActiveHandles, terminateChildProcesses } from './cli/runtime-debug.js'; -import { resolveConfigPath } from './config.js'; +import { resolveConfigPath } from './config/path-discovery.js'; import type { Runtime, RuntimeOptions } from './runtime.js'; export { parseCallArguments } from './cli/call-arguments.js'; @@ -323,6 +323,9 @@ async function maybeHandleDaemonFastCall( if (!server || !DAEMON_FAST_PATH_SERVERS.has(server) || isFastPathKeepAliveDisabled(server)) { return false; } + if (await maybeHandleSimpleDaemonFastCall(callArgs, configResolution, rootDir)) { + return true; + } const [{ DaemonClient }, { handleCall: importedHandleCall }] = await Promise.all([ import('./daemon/client.js'), import('./cli/call-command.js'), @@ -336,6 +339,55 @@ async function maybeHandleDaemonFastCall( return true; } +async function maybeHandleSimpleDaemonFastCall( + callArgs: string[], + configResolution: { path: string; explicit: boolean }, + rootDir: string | undefined +): Promise { + const [{ parseCallArguments }, { resolveCallTimeout }] = await Promise.all([ + import('./cli/call-arguments.js'), + import('./cli/timeouts.js'), + ]); + let parsed: ReturnType; + try { + parsed = parseCallArguments([...callArgs]); + } catch { + return false; + } + if ( + !parsed.server || + !parsed.tool || + parsed.ephemeral || + parsed.tailLog || + parsed.saveImagesDir || + (parsed.positionalArgs?.length ?? 0) > 0 || + parsed.schemaStringCoercionCandidates || + parsed.schemaArrayCoercionCandidates + ) { + return false; + } + + const [{ DaemonClient }, { wrapCallResult }, { printCallOutput }] = await Promise.all([ + import('./daemon/client.js'), + import('./result-utils.js'), + import('./cli/output-utils.js'), + ]); + const daemonClient = new DaemonClient({ + configPath: configResolution.path, + configExplicit: configResolution.explicit, + rootDir, + }); + const result = await daemonClient.callTool({ + server: parsed.server, + tool: parsed.tool, + args: Object.keys(parsed.args).length > 0 ? parsed.args : undefined, + timeoutMs: resolveCallTimeout(parsed.timeoutMs), + }); + const { callResult } = wrapCallResult(result); + printCallOutput(callResult, result, parsed.output); + return true; +} + function resolveDaemonFastCallArgs(command: string, args: string[]): string[] | undefined { if (command === 'call') { return args; diff --git a/src/cli/cli-factory.ts b/src/cli/cli-factory.ts index 206b452..d432ba7 100644 --- a/src/cli/cli-factory.ts +++ b/src/cli/cli-factory.ts @@ -1,4 +1,4 @@ -import { resolveConfigPath } from '../config.js'; +import { resolveConfigPath } from '../config/path-discovery.js'; import { parseLogLevel } from '../logging.js'; import { extractFlags } from './flag-utils.js'; import { getActiveLogger, getActiveLogLevel, logError, setLogLevel } from './logger-context.js'; diff --git a/src/cli/config/doctor.ts b/src/cli/config/doctor.ts index dfdbbf8..afe7987 100644 --- a/src/cli/config/doctor.ts +++ b/src/cli/config/doctor.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { loadServerDefinitions } from '../../config.js'; -import { MCPORTER_VERSION } from '../../runtime.js'; +import { MCPORTER_VERSION } from '../../version.js'; import { logConfigLocations, resolveConfigLocations } from './shared.js'; import type { ConfigCliOptions } from './types.js'; diff --git a/src/cli/generate/artifacts.ts b/src/cli/generate/artifacts.ts index 3f3b0ea..a00b2f1 100644 --- a/src/cli/generate/artifacts.ts +++ b/src/cli/generate/artifacts.ts @@ -5,7 +5,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import type { RolldownPlugin } from 'rolldown'; -import { MCPORTER_VERSION } from '../../runtime.js'; +import { MCPORTER_VERSION } from '../../version.js'; import { markExecutable, safeCopyFile } from './fs-helpers.js'; import { verifyBunAvailable } from './runtime.js'; @@ -76,6 +76,7 @@ async function bundleWithRolldown({ await bundle.write({ file: absTarget, format: runtimeKind === 'bun' ? 'esm' : 'cjs', + codeSplitting: false, sourcemap: false, minify, }); diff --git a/src/cli/generate/template.ts b/src/cli/generate/template.ts index 1bf4fb4..c2e2ceb 100644 --- a/src/cli/generate/template.ts +++ b/src/cli/generate/template.ts @@ -3,7 +3,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import type { CliArtifactMetadata } from '../../cli-metadata.js'; import type { ServerDefinition } from '../../config.js'; -import { MCPORTER_VERSION } from '../../runtime.js'; +import { MCPORTER_VERSION } from '../../version.js'; import { buildToolDoc, type ToolOptionDoc } from '../list-detail-helpers.js'; import { markExecutable } from './fs-helpers.js'; import { renderEmbeddedHelpSource } from './template-help.js'; diff --git a/src/cli/help-output.ts b/src/cli/help-output.ts index 2420975..514ef48 100644 --- a/src/cli/help-output.ts +++ b/src/cli/help-output.ts @@ -1,5 +1,5 @@ import fsPromises from 'node:fs/promises'; -import { MCPORTER_VERSION } from '../runtime.js'; +import { MCPORTER_VERSION } from '../version.js'; import { boldText, dimText, extraDimText, supportsAnsiColor } from './terminal.js'; type HelpEntry = { diff --git a/src/cli/list-command.ts b/src/cli/list-command.ts index 9434b19..e9aa79d 100644 --- a/src/cli/list-command.ts +++ b/src/cli/list-command.ts @@ -1,7 +1,7 @@ import ora from 'ora'; import type { ServerDefinition } from '../config.js'; -import { MCPORTER_VERSION } from '../runtime.js'; import { setStdioLogMode } from '../sdk-patches.js'; +import { MCPORTER_VERSION } from '../version.js'; import { persistPreparedEphemeralServer, prepareEphemeralServerTarget } from './ephemeral-target.js'; import { splitHttpToolSelector } from './http-utils.js'; import { chooseClosestIdentifier, renderIdentifierResolutionMessages } from './identifier-helpers.js'; diff --git a/src/daemon/client.ts b/src/daemon/client.ts index 07e5684..08ec7ac 100644 --- a/src/daemon/client.ts +++ b/src/daemon/client.ts @@ -3,7 +3,6 @@ import fs from 'node:fs/promises'; import net from 'node:net'; import path from 'node:path'; import { listConfigLayerPaths } from '../config/path-discovery.js'; -import { launchDaemonDetached } from './launch.js'; import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js'; import type { CallToolParams, @@ -143,7 +142,8 @@ export class DaemonClient { return; } this.startingPromise = Promise.resolve() - .then(() => { + .then(async () => { + const { launchDaemonDetached } = await import('./launch.js'); launchDaemonDetached({ configPath: this.options.configPath, configExplicit: this.options.configExplicit, @@ -351,7 +351,12 @@ function delay(ms: number): Promise { function normalizeLayers( layers: Array<{ path: string; mtimeMs: number | null }> ): Array<{ path: string; mtimeMs: number | null }> { - return layers - .map((entry) => ({ path: path.resolve(entry.path), mtimeMs: entry.mtimeMs ?? null })) - .toSorted((a, b) => a.path.localeCompare(b.path)); + const normalized = layers.map((entry) => ({ + path: path.isAbsolute(entry.path) ? entry.path : path.resolve(entry.path), + mtimeMs: entry.mtimeMs ?? null, + })); + if (normalized.length < 2) { + return normalized; + } + return normalized.toSorted((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)); } diff --git a/src/runtime.ts b/src/runtime.ts index 0409a07..6f32a20 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,5 +1,3 @@ -import { createRequire } from 'node:module'; - import type { CallToolRequest, ListResourcesRequest, ReadResourceRequest } from '@modelcontextprotocol/sdk/types.js'; import { loadServerDefinitions, type ServerDefinition } from './config.js'; import { createPrefixedConsoleLogger, type Logger, type LogLevel, resolveLogLevelFromEnv } from './logging.js'; @@ -10,17 +8,11 @@ import { resolveOAuthTimeoutFromEnv } from './runtime/oauth.js'; import { type ClientContext, createClientContext } from './runtime/transport.js'; import { normalizeTimeout, raceWithTimeout } from './runtime/utils.js'; import { filterTools, isToolAllowed, validateToolFilters } from './tool-filters.js'; +import { MCPORTER_VERSION } from './version.js'; + +export { MCPORTER_VERSION } from './version.js'; const PACKAGE_NAME = 'mcporter'; -// Keep version in one place by reading package.json; fall back gracefully when bundled without it (e.g., bun bundle). -const CLIENT_VERSION = (() => { - try { - return createRequire(import.meta.url)('../package.json').version as string; - } catch { - return process.env.MCPORTER_VERSION ?? '0.0.0-dev'; - } -})(); -export const MCPORTER_VERSION = CLIENT_VERSION; const OAUTH_CODE_TIMEOUT_MS = resolveOAuthTimeoutFromEnv(); export interface RuntimeOptions { @@ -121,7 +113,7 @@ class McpRuntime implements Runtime { this.logger = options.logger ?? createConsoleLogger(); this.clientInfo = options.clientInfo ?? { name: PACKAGE_NAME, - version: CLIENT_VERSION, + version: MCPORTER_VERSION, }; this.oauthTimeoutMs = options.oauthTimeoutMs; } diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..ee4b7c3 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,9 @@ +import { createRequire } from 'node:module'; + +export const MCPORTER_VERSION = (() => { + try { + return createRequire(import.meta.url)('../package.json').version as string; + } catch { + return process.env.MCPORTER_VERSION ?? '0.0.0-dev'; + } +})(); diff --git a/tests/daemon-client-config-stale.test.ts b/tests/daemon-client-config-stale.test.ts index 347ccd2..d0dc3d2 100644 --- a/tests/daemon-client-config-stale.test.ts +++ b/tests/daemon-client-config-stale.test.ts @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { makeShortTempDir } from './fixtures/test-helpers.js'; const sentMethods: string[] = []; -let launchDaemonDetached: ReturnType; +const launchDaemonDetached = vi.hoisted(() => vi.fn()); let createConnection: ReturnType; class MockSocket extends EventEmitter { @@ -73,7 +73,6 @@ vi.mock('node:net', () => { }); vi.mock('../src/daemon/launch.js', () => { - launchDaemonDetached = vi.fn(); return { launchDaemonDetached }; });