fix(daemon): keep stdio list requests warm
This commit is contained in:
parent
815016a008
commit
56be50f763
@ -9,6 +9,7 @@
|
||||
### CLI
|
||||
|
||||
- Reconcile keep-alive daemon metadata with the responding process and serialize daemon startup across parallel clients, preventing duplicate orphaned daemons. (Issue #191, thanks @dtmsyi)
|
||||
- Keep daemon-managed stdio servers warm across repeated `mcporter list` requests instead of treating non-interactive tool listing as a throwaway process. (Issue #188, thanks @robertoronderosjr)
|
||||
|
||||
## [0.11.3] - 2026-05-21
|
||||
|
||||
|
||||
@ -343,6 +343,7 @@ async function processRequest(
|
||||
case 'listTools': {
|
||||
const params = request.params as ListToolsParams;
|
||||
ensureManaged(params.server, managedServers);
|
||||
const definition = managedServers.get(params.server)!;
|
||||
const loggable = shouldLogServer(logContext, params.server);
|
||||
if (loggable) {
|
||||
logEvent(logContext, `listTools start server=${params.server}`);
|
||||
@ -350,7 +351,7 @@ async function processRequest(
|
||||
try {
|
||||
const result = await runtime.listTools(params.server, {
|
||||
includeSchema: params.includeSchema,
|
||||
autoAuthorize: params.autoAuthorize,
|
||||
autoAuthorize: resolveDaemonListToolsAutoAuthorize(params, definition),
|
||||
allowCachedAuth: params.allowCachedAuth ?? true,
|
||||
});
|
||||
markActivity(params.server, activity);
|
||||
@ -476,6 +477,16 @@ async function processRequest(
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDaemonListToolsAutoAuthorize(
|
||||
params: ListToolsParams,
|
||||
definition: ServerDefinition
|
||||
): boolean | undefined {
|
||||
if (params.autoAuthorize === false && definition.command.kind === 'stdio') {
|
||||
return undefined;
|
||||
}
|
||||
return params.autoAuthorize;
|
||||
}
|
||||
|
||||
export async function __testProcessRequest(
|
||||
rawPayload: string,
|
||||
runtime: Runtime,
|
||||
|
||||
@ -59,6 +59,40 @@ describe('daemon host request handling', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps stdio keep-alive listTools requests reusable when callers disable auto auth', async () => {
|
||||
const runtime = createRuntimeDouble();
|
||||
const managedServers = createManagedServers();
|
||||
|
||||
await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
|
||||
id: 'list',
|
||||
method: 'listTools',
|
||||
params: { server: 'local', includeSchema: true, autoAuthorize: false, allowCachedAuth: true },
|
||||
});
|
||||
|
||||
expect(runtime.listTools).toHaveBeenCalledWith('local', {
|
||||
includeSchema: true,
|
||||
autoAuthorize: undefined,
|
||||
allowCachedAuth: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves HTTP listTools auto-auth opt out on daemon requests', async () => {
|
||||
const runtime = createRuntimeDouble();
|
||||
const managedServers = createManagedServers();
|
||||
|
||||
await __testProcessRequest('', runtime as unknown as Runtime, managedServers, new Map(), metadata, logContext, {
|
||||
id: 'list',
|
||||
method: 'listTools',
|
||||
params: { server: 'oauth', includeSchema: true, autoAuthorize: false, allowCachedAuth: true },
|
||||
});
|
||||
|
||||
expect(runtime.listTools).toHaveBeenCalledWith('oauth', {
|
||||
includeSchema: true,
|
||||
autoAuthorize: false,
|
||||
allowCachedAuth: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves explicit listTools cached-auth opt out on daemon requests', async () => {
|
||||
const runtime = createRuntimeDouble();
|
||||
const managedServers = createManagedServers();
|
||||
@ -86,6 +120,14 @@ function createRuntimeDouble(): Pick<Runtime, 'callTool' | 'listTools'> {
|
||||
|
||||
function createManagedServers(): Map<string, ServerDefinition> {
|
||||
return new Map([
|
||||
[
|
||||
'local',
|
||||
{
|
||||
name: 'local',
|
||||
command: { kind: 'stdio', command: 'node', args: ['server.js'], cwd: '/tmp' },
|
||||
lifecycle: { mode: 'keep-alive' },
|
||||
},
|
||||
],
|
||||
[
|
||||
'oauth',
|
||||
{
|
||||
|
||||
@ -77,8 +77,10 @@ describeDaemon('daemon keep-alive integration', () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-daemon-e2e-'));
|
||||
const scriptPath = path.join(tempDir, 'daemon-server.mjs');
|
||||
const configPath = path.join(tempDir, 'mcporter.daemon.json');
|
||||
const launchLogPath = path.join(tempDir, 'launches.log');
|
||||
|
||||
const stdioServerSource = `import { randomUUID } from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import { McpServer } from '${MCP_SERVER_MODULE}';
|
||||
import { StdioServerTransport } from '${STDIO_SERVER_MODULE}';
|
||||
import { z } from '${ZOD_MODULE}';
|
||||
@ -86,6 +88,10 @@ import { z } from '${ZOD_MODULE}';
|
||||
const instanceId = randomUUID();
|
||||
let counter = 0;
|
||||
|
||||
if (process.env.MCPORTER_TEST_LAUNCH_LOG) {
|
||||
await fs.appendFile(process.env.MCPORTER_TEST_LAUNCH_LOG, instanceId + '\\n', 'utf8');
|
||||
}
|
||||
|
||||
const server = new McpServer({ name: 'daemon-e2e', version: '1.0.0' });
|
||||
server.registerTool('next_value', {
|
||||
title: 'Next value',
|
||||
@ -135,12 +141,16 @@ await new Promise((resolve) => {
|
||||
MCPORTER_DAEMON_LOG: '1',
|
||||
MCPORTER_DAEMON_LOG_PATH: logPath,
|
||||
MCPORTER_DAEMON_LOG_SERVERS: 'daemon-e2e',
|
||||
MCPORTER_TEST_LAUNCH_LOG: launchLogPath,
|
||||
};
|
||||
const cli = (args: string[]) => runCli(args, configPath, cliEnv);
|
||||
|
||||
try {
|
||||
await cli(['daemon', 'stop']);
|
||||
|
||||
await cli(['list', 'daemon-e2e', '--json']);
|
||||
await cli(['list', 'daemon-e2e', '--json']);
|
||||
|
||||
const first = await cli(['call', 'daemon-e2e.next_value', '--output', 'json']);
|
||||
const firstResult = parseCliJson(first.stdout);
|
||||
expect(firstResult.count).toBe(1);
|
||||
@ -150,7 +160,12 @@ await new Promise((resolve) => {
|
||||
expect(secondResult.count).toBe(2);
|
||||
expect(secondResult.instanceId).toBe(firstResult.instanceId);
|
||||
|
||||
const launchLog = await readFileWithRetries(launchLogPath);
|
||||
expect(launchLog.trim().split('\n')).toEqual([firstResult.instanceId]);
|
||||
|
||||
const logContents = await readFileWithRetries(logPath);
|
||||
expect(logContents).toContain('listTools start server=daemon-e2e');
|
||||
expect(logContents).toContain('listTools success server=daemon-e2e');
|
||||
expect(logContents).toContain('callTool start server=daemon-e2e tool=next_value');
|
||||
expect(logContents).toContain('callTool success server=daemon-e2e tool=next_value');
|
||||
} finally {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user