Skip to content

Commit 78ef292

Browse files
feat: introduces dry mode for CLI
1 parent e4d3d7b commit 78ef292

File tree

8 files changed

+138
-2
lines changed

8 files changed

+138
-2
lines changed

src/common/config/argsParserOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const OPTIONS = {
6060
"apiDeprecationErrors",
6161
"apiStrict",
6262
"disableEmbeddingsValidation",
63+
"dry",
6364
"help",
6465
"indexCheck",
6566
"ipv6",

src/common/config/userConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,10 @@ export const UserConfigSchema = z4.object({
167167
)
168168
.default([])
169169
.describe("An array of preview features that are enabled."),
170+
dry: z4
171+
.boolean()
172+
.default(false)
173+
.describe(
174+
"When true, runs the server in dry mode: dumps configuration and enabled tools, then exits without starting the server."
175+
),
170176
});

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ import { StdioRunner } from "./transports/stdio.js";
4444
import { StreamableHttpRunner } from "./transports/streamableHttp.js";
4545
import { systemCA } from "@mongodb-js/devtools-proxy-support";
4646
import { Keychain } from "./common/keychain.js";
47+
import { DryModeRunner } from "./transports/dryModeRunner.js";
4748

4849
async function main(): Promise<void> {
4950
systemCA().catch(() => undefined); // load system CA asynchronously as in mongosh
5051

5152
const config = createUserConfig();
5253
assertHelpMode(config);
5354
assertVersionMode(config);
55+
await DryModeRunner.assertDryMode({ userConfig: config });
5456

5557
const transportRunner =
5658
config.transport === "stdio"

src/transports/dryModeRunner.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { InMemoryTransport } from "./inMemoryTransport.js";
2+
import { TransportRunnerBase, type TransportRunnerConfig } from "./base.js";
3+
import { type Server } from "../server.js";
4+
5+
export type DryModeTestHelpers = {
6+
exit(this: void, exitCode: number): never;
7+
logger: {
8+
log(this: void, message: string): void;
9+
error(this: void, message: string): void;
10+
};
11+
};
12+
13+
type DryModeRunnerConfig = TransportRunnerConfig & DryModeTestHelpers;
14+
15+
const defaultLogger: DryModeTestHelpers["logger"] = {
16+
log(message) {
17+
console.warn(message);
18+
},
19+
error(message) {
20+
console.error(message);
21+
},
22+
};
23+
24+
export class DryModeRunner extends TransportRunnerBase {
25+
private server: Server | undefined;
26+
private exitProcess: DryModeTestHelpers["exit"];
27+
private consoleLogger: DryModeTestHelpers["logger"];
28+
29+
constructor({ exit, logger, ...transportRunnerConfig }: DryModeRunnerConfig) {
30+
super(transportRunnerConfig);
31+
this.exitProcess = exit;
32+
this.consoleLogger = logger;
33+
}
34+
35+
async start(): Promise<void> {
36+
try {
37+
this.server = await this.setupServer();
38+
const transport = new InMemoryTransport();
39+
40+
await this.server.connect(transport);
41+
} catch (error: unknown) {
42+
this.consoleLogger.error(`Fatal error running server: ${error as string}`);
43+
this.exitProcess(1);
44+
}
45+
}
46+
47+
async closeTransport(): Promise<void> {
48+
await this.server?.close();
49+
}
50+
51+
private dumpConfig(): void {
52+
this.consoleLogger.log("Configuration:");
53+
this.consoleLogger.log(JSON.stringify(this.userConfig, null, 2));
54+
}
55+
56+
private dumpTools(): void {
57+
const tools = this.server?.tools.map((tool) => ({
58+
name: tool.name,
59+
description: tool.description,
60+
category: tool.category,
61+
}));
62+
this.consoleLogger.log("Enabled tools:");
63+
this.consoleLogger.log(JSON.stringify(tools, null, 2));
64+
}
65+
66+
static async assertDryMode(
67+
runnerConfig: TransportRunnerConfig,
68+
exit: DryModeTestHelpers["exit"] = (exitCode: number) => process.exit(exitCode),
69+
logger: DryModeTestHelpers["logger"] = defaultLogger
70+
): Promise<void> | never {
71+
if (runnerConfig.userConfig.dry) {
72+
const runner = new this({ ...runnerConfig, exit, logger });
73+
await runner.start();
74+
runner.dumpConfig();
75+
runner.dumpTools();
76+
exit(0);
77+
}
78+
}
79+
}
File renamed without changes.

tests/integration/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Server, type ServerOptions } from "../../src/server.js";
55
import { Telemetry } from "../../src/telemetry/telemetry.js";
66
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8-
import { InMemoryTransport } from "./inMemoryTransport.js";
8+
import { InMemoryTransport } from "../../src/transports/inMemoryTransport.js";
99
import { type UserConfig } from "../../src/common/config/userConfig.js";
1010
import { ResourceUpdatedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
1111
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";

tests/integration/tools/mongodb/mongodbTool.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Session } from "../../../../src/common/session.js";
1010
import { CompositeLogger } from "../../../../src/common/logger.js";
1111
import { DeviceId } from "../../../../src/helpers/deviceId.js";
1212
import { ExportsManager } from "../../../../src/common/exportsManager.js";
13-
import { InMemoryTransport } from "../../inMemoryTransport.js";
13+
import { InMemoryTransport } from "../../../../src/transports/inMemoryTransport.js";
1414
import { Telemetry } from "../../../../src/telemetry/telemetry.js";
1515
import { Server } from "../../../../src/server.js";
1616
import { type ConnectionErrorHandler, connectionErrorHandler } from "../../../../src/common/connectionErrorHandler.js";
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { DryModeRunner, type DryModeTestHelpers } from "../../../src/transports/dryModeRunner.js";
3+
import { type UserConfig } from "../../../src/common/config/userConfig.js";
4+
import { type TransportRunnerConfig } from "../../../src/transports/base.js";
5+
import { defaultTestConfig } from "../../integration/helpers.js";
6+
7+
describe("DryModeRunner", () => {
8+
let exitMock: DryModeTestHelpers["exit"];
9+
let loggerMock: DryModeTestHelpers["logger"];
10+
let runnerConfig: TransportRunnerConfig;
11+
12+
beforeEach(() => {
13+
exitMock = vi.fn<DryModeTestHelpers["exit"]>();
14+
loggerMock = {
15+
log: vi.fn(),
16+
error: vi.fn(),
17+
};
18+
runnerConfig = {
19+
userConfig: defaultTestConfig,
20+
} as TransportRunnerConfig;
21+
});
22+
23+
afterEach(() => {
24+
vi.clearAllMocks();
25+
});
26+
27+
it("should not do anything if dry mode is disabled", async () => {
28+
await DryModeRunner.assertDryMode(runnerConfig, exitMock, loggerMock);
29+
expect(exitMock).not.toHaveBeenCalled();
30+
expect(loggerMock.log).not.toHaveBeenCalled();
31+
});
32+
33+
it.each([{ transport: "http", httpHost: "127.0.0.1", httpPort: "3001" }, { transport: "stdio" }] as Array<
34+
Partial<UserConfig>
35+
>)("should run in dry mode if enabled for transport - $transport", async (partialConfig) => {
36+
runnerConfig.userConfig = {
37+
...runnerConfig.userConfig,
38+
...partialConfig,
39+
dry: true,
40+
};
41+
await DryModeRunner.assertDryMode(runnerConfig, exitMock, loggerMock);
42+
expect(exitMock).toHaveBeenCalledWith(0);
43+
expect(loggerMock.log).toHaveBeenNthCalledWith(1, "Configuration:");
44+
expect(loggerMock.log).toHaveBeenNthCalledWith(2, JSON.stringify(runnerConfig.userConfig, null, 2));
45+
expect(loggerMock.log).toHaveBeenNthCalledWith(3, "Enabled tools:");
46+
expect(loggerMock.log).toHaveBeenNthCalledWith(4, expect.stringContaining('"name": "connect"'));
47+
});
48+
});

0 commit comments

Comments
 (0)