perf: speed up daemon fast-path calls

This commit is contained in:
Peter Steinberger 2026-05-09 11:54:05 +01:00
parent f412d42122
commit e6b451aee3
No known key found for this signature in database
12 changed files with 85 additions and 26 deletions

View File

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

View File

@ -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<boolean> {
const [{ parseCallArguments }, { resolveCallTimeout }] = await Promise.all([
import('./cli/call-arguments.js'),
import('./cli/timeouts.js'),
]);
let parsed: ReturnType<typeof parseCallArguments>;
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<void> {
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));
}

View File

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

9
src/version.ts Normal file
View File

@ -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';
}
})();

View File

@ -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<typeof vi.fn>;
const launchDaemonDetached = vi.hoisted(() => vi.fn());
let createConnection: ReturnType<typeof vi.fn>;
class MockSocket extends EventEmitter {
@ -73,7 +73,6 @@ vi.mock('node:net', () => {
});
vi.mock('../src/daemon/launch.js', () => {
launchDaemonDetached = vi.fn();
return { launchDaemonDetached };
});