Skip to content

Commit 17cdcd1

Browse files
feat: introduces dry mode for CLI MCP-297 (#747)
1 parent cfb965b commit 17cdcd1

File tree

11 files changed

+210
-83
lines changed

11 files changed

+210
-83
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
"generate": "pnpm run generate:api && pnpm run generate:arguments",
6767
"generate:api": "./scripts/generate.sh",
6868
"generate:arguments": "tsx scripts/generateArguments.ts",
69+
"pretest": "pnpm run build",
6970
"test": "vitest --project eslint-rules --project unit-and-integration --coverage",
70-
"pretest:accuracy": "pnpm run build",
7171
"test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh",
7272
"test:long-running-tests": "vitest --project long-running-tests --coverage",
7373
"atlas:cleanup": "vitest --project atlas-cleanup"

src/common/config/argsParserOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const OPTIONS = {
5959
boolean: [
6060
"apiDeprecationErrors",
6161
"apiStrict",
62+
"dryRun",
6263
"embeddingsValidation",
6364
"help",
6465
"indexCheck",

src/common/config/userConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,10 @@ export const UserConfigSchema = z4.object({
173173
)
174174
.default([])
175175
.describe("An array of preview features that are enabled."),
176+
dryRun: z4
177+
.boolean()
178+
.default(false)
179+
.describe(
180+
"When true, runs the server in dry mode: dumps configuration and enabled tools, then exits without starting the server."
181+
),
176182
});

src/index.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,23 @@ 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 { DryRunModeRunner } 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();
52-
assertHelpMode(config);
53-
assertVersionMode(config);
53+
if (config.help) {
54+
handleHelpRequest();
55+
}
56+
57+
if (config.version) {
58+
handleVersionRequest();
59+
}
60+
61+
if (config.dryRun) {
62+
await handleDryRunRequest(config);
63+
}
5464

5565
const transportRunner =
5666
config.transport === "stdio"
@@ -133,17 +143,35 @@ main().catch((error: unknown) => {
133143
process.exit(1);
134144
});
135145

136-
function assertHelpMode(config: UserConfig): void | never {
137-
if (config.help) {
138-
console.log("For usage information refer to the README.md:");
139-
console.log("https://github.com/mongodb-js/mongodb-mcp-server?tab=readme-ov-file#quick-start");
140-
process.exit(0);
141-
}
146+
function handleHelpRequest(): never {
147+
console.log("For usage information refer to the README.md:");
148+
console.log("https://github.com/mongodb-js/mongodb-mcp-server?tab=readme-ov-file#quick-start");
149+
process.exit(0);
142150
}
143151

144-
function assertVersionMode(config: UserConfig): void | never {
145-
if (config.version) {
146-
console.log(packageInfo.version);
152+
function handleVersionRequest(): never {
153+
console.log(packageInfo.version);
154+
process.exit(0);
155+
}
156+
157+
export async function handleDryRunRequest(config: UserConfig): Promise<never> {
158+
try {
159+
const runner = new DryRunModeRunner({
160+
userConfig: config,
161+
logger: {
162+
log(message): void {
163+
console.log(message);
164+
},
165+
error(message): void {
166+
console.error(message);
167+
},
168+
},
169+
});
170+
await runner.start();
171+
await runner.close();
147172
process.exit(0);
173+
} catch (error) {
174+
console.error(`Fatal error running server in dry run mode: ${error as string}`);
175+
process.exit(1);
148176
}
149177
}

src/transports/dryModeRunner.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 DryRunModeTestHelpers = {
6+
logger: {
7+
log(this: void, message: string): void;
8+
error(this: void, message: string): void;
9+
};
10+
};
11+
12+
type DryRunModeRunnerConfig = TransportRunnerConfig & DryRunModeTestHelpers;
13+
14+
export class DryRunModeRunner extends TransportRunnerBase {
15+
private server: Server | undefined;
16+
private consoleLogger: DryRunModeTestHelpers["logger"];
17+
18+
constructor({ logger, ...transportRunnerConfig }: DryRunModeRunnerConfig) {
19+
super(transportRunnerConfig);
20+
this.consoleLogger = logger;
21+
}
22+
23+
override async start(): Promise<void> {
24+
this.server = await this.setupServer();
25+
const transport = new InMemoryTransport();
26+
27+
await this.server.connect(transport);
28+
this.dumpConfig();
29+
this.dumpTools();
30+
}
31+
32+
override async closeTransport(): Promise<void> {
33+
await this.server?.close();
34+
}
35+
36+
private dumpConfig(): void {
37+
this.consoleLogger.log("Configuration:");
38+
this.consoleLogger.log(JSON.stringify(this.userConfig, null, 2));
39+
}
40+
41+
private dumpTools(): void {
42+
const tools =
43+
this.server?.tools
44+
.filter((tool) => tool.isEnabled())
45+
.map((tool) => ({
46+
name: tool.name,
47+
category: tool.category,
48+
})) ?? [];
49+
this.consoleLogger.log("Enabled tools:");
50+
this.consoleLogger.log(JSON.stringify(tools, null, 2));
51+
}
52+
}
File renamed without changes.

tests/e2e/cli.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import path from "path";
2+
import { execFile } from "child_process";
3+
import { promisify } from "util";
4+
import { describe, expect, it } from "vitest";
5+
import packageJson from "../../package.json" with { type: "json" };
6+
7+
const execFileAsync = promisify(execFile);
8+
const CLI_PATH = path.join(import.meta.dirname, "..", "..", "dist", "index.js");
9+
10+
describe("CLI entrypoint", () => {
11+
it("should handle version request", async () => {
12+
const { stdout, stderr } = await execFileAsync(process.execPath, [CLI_PATH, "--version"]);
13+
expect(stdout).toContain(packageJson.version);
14+
expect(stderr).toEqual("");
15+
});
16+
17+
it("should handle help request", async () => {
18+
const { stdout, stderr } = await execFileAsync(process.execPath, [CLI_PATH, "--help"]);
19+
expect(stdout).toContain("For usage information refer to the README.md");
20+
expect(stderr).toEqual("");
21+
});
22+
23+
it("should handle dry run request", async () => {
24+
const { stdout } = await execFileAsync(process.execPath, [CLI_PATH, "--dryRun"]);
25+
expect(stdout).toContain("Configuration:");
26+
expect(stdout).toContain("Enabled tools:");
27+
// We don't do stderr assertions because in our CI, for docker-less env
28+
// atlas local tools push message on stderr stream.
29+
});
30+
});

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";

tests/unit/common/config.test.ts

Lines changed: 37 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -25,81 +25,49 @@ function createEnvironment(): {
2525
};
2626
}
2727

28+
// Expected hardcoded values (what we had before)
29+
const expectedDefaults = {
30+
apiBaseUrl: "https://cloud.mongodb.com/",
31+
logPath: getLogPath(),
32+
exportsPath: getExportsPath(),
33+
exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
34+
exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
35+
disabledTools: [],
36+
telemetry: "enabled",
37+
readOnly: false,
38+
indexCheck: false,
39+
confirmationRequiredTools: [
40+
"atlas-create-access-list",
41+
"atlas-create-db-user",
42+
"drop-database",
43+
"drop-collection",
44+
"delete-many",
45+
"drop-index",
46+
],
47+
transport: "stdio",
48+
httpPort: 3000,
49+
httpHost: "127.0.0.1",
50+
loggers: ["disk", "mcp"],
51+
idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
52+
notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
53+
httpHeaders: {},
54+
maxDocumentsPerQuery: 100,
55+
maxBytesPerQuery: 16 * 1024 * 1024, // ~16 mb
56+
atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
57+
voyageApiKey: "",
58+
vectorSearchDimensions: 1024,
59+
vectorSearchSimilarityFunction: "euclidean",
60+
embeddingsValidation: true,
61+
previewFeatures: [],
62+
dryRun: false,
63+
};
64+
2865
describe("config", () => {
2966
it("should generate defaults from UserConfigSchema that match expected values", () => {
30-
// Expected hardcoded values (what we had before)
31-
const expectedDefaults = {
32-
apiBaseUrl: "https://cloud.mongodb.com/",
33-
logPath: getLogPath(),
34-
exportsPath: getExportsPath(),
35-
exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
36-
exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
37-
disabledTools: [],
38-
telemetry: "enabled",
39-
readOnly: false,
40-
indexCheck: false,
41-
confirmationRequiredTools: [
42-
"atlas-create-access-list",
43-
"atlas-create-db-user",
44-
"drop-database",
45-
"drop-collection",
46-
"delete-many",
47-
"drop-index",
48-
],
49-
transport: "stdio",
50-
httpPort: 3000,
51-
httpHost: "127.0.0.1",
52-
loggers: ["disk", "mcp"],
53-
idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
54-
notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
55-
httpHeaders: {},
56-
maxDocumentsPerQuery: 100,
57-
maxBytesPerQuery: 16 * 1024 * 1024, // ~16 mb
58-
atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
59-
voyageApiKey: "",
60-
vectorSearchDimensions: 1024,
61-
vectorSearchSimilarityFunction: "euclidean",
62-
embeddingsValidation: true,
63-
previewFeatures: [],
64-
};
6567
expect(UserConfigSchema.parse({})).toStrictEqual(expectedDefaults);
6668
});
6769

6870
it("should generate defaults when no config sources are populated", () => {
69-
const expectedDefaults = {
70-
apiBaseUrl: "https://cloud.mongodb.com/",
71-
logPath: getLogPath(),
72-
exportsPath: getExportsPath(),
73-
exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
74-
exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
75-
disabledTools: [],
76-
telemetry: "enabled",
77-
readOnly: false,
78-
indexCheck: false,
79-
confirmationRequiredTools: [
80-
"atlas-create-access-list",
81-
"atlas-create-db-user",
82-
"drop-database",
83-
"drop-collection",
84-
"delete-many",
85-
"drop-index",
86-
],
87-
transport: "stdio",
88-
httpPort: 3000,
89-
httpHost: "127.0.0.1",
90-
loggers: ["disk", "mcp"],
91-
idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
92-
notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
93-
httpHeaders: {},
94-
maxDocumentsPerQuery: 100,
95-
maxBytesPerQuery: 16 * 1024 * 1024, // ~16 mb
96-
atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
97-
voyageApiKey: "",
98-
vectorSearchDimensions: 1024,
99-
vectorSearchSimilarityFunction: "euclidean",
100-
embeddingsValidation: true,
101-
previewFeatures: [],
102-
};
10371
expect(createUserConfig()).toStrictEqual(expectedDefaults);
10472
});
10573

0 commit comments

Comments
 (0)