Skip to content

Commit 8b9cc4e

Browse files
authored
chore: adopt strict validation for all command line arguments MCP-298 (#777)
1 parent 18ff7cc commit 8b9cc4e

25 files changed

+954
-798
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ coverage
1414
# Generated assets by accuracy runs
1515
.accuracy
1616

17-
.DS_Store
17+
.DS_Store
18+
19+
# Development tool files
20+
.yalc
21+
yalc.lock

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
358358
| `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | `120000` | Time in milliseconds between export cleanup cycles that remove expired export files. |
359359
| `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | `300000` | Time in milliseconds after which an export is considered expired and eligible for cleanup. |
360360
| `exportsPath` | `MDB_MCP_EXPORTS_PATH` | see below\* | Folder to store exported data files. |
361+
| `httpHeaders` | `MDB_MCP_HTTP_HEADERS` | `"{}"` | Header that the HTTP server will validate when making requests (only used when transport is 'http'). |
361362
| `httpHost` | `MDB_MCP_HTTP_HOST` | `"127.0.0.1"` | Host address to bind the HTTP server to (only used when transport is 'http'). |
362363
| `httpPort` | `MDB_MCP_HTTP_PORT` | `3000` | Port number for the HTTP server (only used when transport is 'http'). Use 0 for a random port. |
363364
| `idleTimeoutMs` | `MDB_MCP_IDLE_TIMEOUT_MS` | `600000` | Idle timeout for a client to disconnect (only applies to http transport). |
@@ -371,6 +372,8 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
371372
| `readOnly` | `MDB_MCP_READ_ONLY` | `false` | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. |
372373
| `telemetry` | `MDB_MCP_TELEMETRY` | `"enabled"` | When set to disabled, disables telemetry collection. |
373374
| `transport` | `MDB_MCP_TRANSPORT` | `"stdio"` | Either 'stdio' or 'http'. |
375+
| `vectorSearchDimensions` | `MDB_MCP_VECTOR_SEARCH_DIMENSIONS` | `1024` | Default number of dimensions for vector search embeddings. |
376+
| `vectorSearchSimilarityFunction` | `MDB_MCP_VECTOR_SEARCH_SIMILARITY_FUNCTION` | `"euclidean"` | Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'. |
374377
| `voyageApiKey` | `MDB_MCP_VOYAGE_API_KEY` | `""` | API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion). |
375378

376379
#### Logger Options

eslint-rules/enforce-zod-v4.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import path from "path";
33

44
// The file that is allowed to import from zod/v4
5-
const configFilePath = path.resolve(import.meta.dirname, "../src/common/config/userConfig.ts");
5+
const allowedFilePaths = [
6+
path.resolve(import.meta.dirname, "../src/common/config/userConfig.ts"),
7+
path.resolve(import.meta.dirname, "../src/common/config/createUserConfig.ts"),
8+
];
69

710
// Ref: https://eslint.org/docs/latest/extend/custom-rules
811
export default {
@@ -23,7 +26,7 @@ export default {
2326
const currentFilePath = path.resolve(context.getFilename());
2427

2528
// Only allow zod v4 import in config.ts
26-
if (currentFilePath === configFilePath) {
29+
if (allowedFilePaths.includes(currentFilePath)) {
2730
return {};
2831
}
2932

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default defineConfig([
8888
"src/types/*.d.ts",
8989
"tests/integration/fixtures/",
9090
"eslint-rules",
91+
".yalc",
9192
]),
9293
eslintPluginPrettierRecommended,
9394
]);

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@
118118
"@modelcontextprotocol/sdk": "^1.24.2",
119119
"@mongodb-js/device-id": "^0.3.1",
120120
"@mongodb-js/devtools-proxy-support": "^0.5.3",
121-
"@mongosh/arg-parser": "^3.19.0",
121+
"@mongosh/arg-parser": "^3.23.0",
122122
"@mongosh/service-provider-node-driver": "^3.17.5",
123123
"ai": "^5.0.72",
124124
"bson": "^6.10.4",
@@ -134,7 +134,6 @@
134134
"openapi-fetch": "^0.15.0",
135135
"ts-levenshtein": "^1.0.7",
136136
"voyage-ai-provider": "^2.0.0",
137-
"yargs-parser": "21.1.1",
138137
"zod": "^3.25.76"
139138
},
140139
"engines": {

pnpm-lock.yaml

Lines changed: 21 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/apply.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { parseArgs } from "@mongosh/arg-parser/arg-parser";
12
import fs from "fs/promises";
23
import type { OpenAPIV3_1 } from "openapi-types";
3-
import argv from "yargs-parser";
4+
import z4 from "zod/v4";
45

56
function findObjectFromRef<T>(obj: T | OpenAPIV3_1.ReferenceObject, openapi: OpenAPIV3_1.Document): T {
67
const ref = (obj as OpenAPIV3_1.ReferenceObject).$ref;
@@ -23,14 +24,16 @@ function findObjectFromRef<T>(obj: T | OpenAPIV3_1.ReferenceObject, openapi: Ope
2324
}
2425

2526
async function main(): Promise<void> {
26-
const { spec, file } = argv(process.argv.slice(2));
27+
const {
28+
parsed: { spec, file },
29+
} = parseArgs({ args: process.argv.slice(2), schema: z4.object({ spec: z4.string(), file: z4.string() }) });
2730

2831
if (!spec || !file) {
2932
console.error("Please provide both --spec and --file arguments.");
3033
process.exit(1);
3134
}
3235

33-
const specFile = await fs.readFile(spec as string, "utf8");
36+
const specFile = await fs.readFile(spec, "utf8");
3437

3538
const operations: {
3639
path: string;
@@ -112,7 +115,7 @@ async ${methodName}(options${requiredParams ? "" : "?"}: FetchOptions<operations
112115
})
113116
.join("\n");
114117

115-
const templateFile = await fs.readFile(file as string, "utf8");
118+
const templateFile = await fs.readFile(file, "utf8");
116119
const templateLines = templateFile.split("\n");
117120
const outputLines: string[] = [];
118121
let addLines = true;
@@ -131,7 +134,7 @@ async ${methodName}(options${requiredParams ? "" : "?"}: FetchOptions<operations
131134
}
132135
const output = outputLines.join("\n");
133136

134-
await fs.writeFile(file as string, output, "utf8");
137+
await fs.writeFile(file, output, "utf8");
135138
}
136139

137140
main().catch((error) => {

scripts/generateArguments.ts

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
* - server.json arrays
66
* - README.md configuration table
77
*
8-
* It uses the Zod schema and OPTIONS defined in src/common/config.ts
8+
* It uses the UserConfig Zod Schema.
99
*/
1010

1111
import { readFileSync, writeFileSync } from "fs";
1212
import { join, dirname } from "path";
1313
import { fileURLToPath } from "url";
1414
import { UserConfigSchema, configRegistry } from "../src/common/config/userConfig.js";
1515
import { execSync } from "child_process";
16-
import { OPTIONS } from "../src/common/config/argsParserOptions.js";
16+
import type { z as z4 } from "zod/v4";
1717

1818
const __filename = fileURLToPath(import.meta.url);
1919
const __dirname = dirname(__filename);
@@ -54,6 +54,54 @@ interface ConfigMetadata {
5454
defaultValue?: unknown;
5555
defaultValueDescription?: string;
5656
isSecret?: boolean;
57+
type: "string" | "number" | "boolean" | "array";
58+
}
59+
60+
/**
61+
* Derives the primitive type from a Zod schema by unwrapping wrappers like default, optional, preprocess, etc.
62+
*/
63+
function deriveZodType(schema: z4.ZodType): "string" | "number" | "boolean" | "array" {
64+
const def = schema.def as unknown as Record<string, unknown>;
65+
const typeName = def.type as string;
66+
67+
// Handle wrapped types (default, optional, nullable, etc.)
68+
if (typeName === "default" || typeName === "optional" || typeName === "nullable") {
69+
const innerType = def.innerType as z4.ZodType;
70+
return deriveZodType(innerType);
71+
}
72+
73+
// Handle preprocess - look at the schema being processed into
74+
if (typeName === "pipe") {
75+
const out = def.out as z4.ZodType;
76+
return deriveZodType(out);
77+
}
78+
79+
// Handle coerce wrapper
80+
if (typeName === "coerce") {
81+
const innerType = def.innerType as z4.ZodType;
82+
return deriveZodType(innerType);
83+
}
84+
85+
// Handle primitive types
86+
if (typeName === "string" || typeName === "enum") {
87+
return "string";
88+
}
89+
if (typeName === "number" || typeName === "int") {
90+
return "number";
91+
}
92+
if (typeName === "boolean") {
93+
return "boolean";
94+
}
95+
if (typeName === "array") {
96+
return "array";
97+
}
98+
if (typeName === "object") {
99+
// Objects are treated as strings (JSON strings)
100+
return "string";
101+
}
102+
103+
// Default fallback
104+
return "string";
57105
}
58106

59107
function extractZodDescriptions(): Record<string, ConfigMetadata> {
@@ -67,6 +115,8 @@ function extractZodDescriptions(): Record<string, ConfigMetadata> {
67115
// Extract description from Zod schema
68116
let description = schema.description || `Configuration option: ${key}`;
69117

118+
const derivedType = deriveZodType(schema);
119+
70120
if ("innerType" in schema.def) {
71121
// "pipe" is also used for our comma-separated arrays
72122
if (schema.def.innerType.def.type === "pipe") {
@@ -93,31 +143,22 @@ function extractZodDescriptions(): Record<string, ConfigMetadata> {
93143
defaultValue,
94144
defaultValueDescription,
95145
isSecret,
146+
type: derivedType,
96147
};
97148
}
98149

99150
return result;
100151
}
101152

102-
function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] {
153+
function getArgumentInfo(zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] {
103154
const argumentInfos: ArgumentInfo[] = [];
104-
const processedKeys = new Set<string>();
105-
106-
// Helper to add env var
107-
const addEnvVar = (key: string, type: "string" | "number" | "boolean" | "array"): void => {
108-
if (processedKeys.has(key)) return;
109-
processedKeys.add(key);
110155

156+
for (const [key, metadata] of Object.entries(zodMetadata)) {
111157
const envVarName = `MDB_MCP_${camelCaseToSnakeCase(key)}`;
112158

113-
// Get description and default value from Zod metadata
114-
const metadata = zodMetadata[key] || {
115-
description: `Configuration option: ${key}`,
116-
};
117-
118159
// Determine format based on type
119-
let format = type;
120-
if (type === "array") {
160+
let format: string = metadata.type;
161+
if (metadata.type === "array") {
121162
format = "string"; // Arrays are passed as comma-separated strings
122163
}
123164

@@ -131,26 +172,6 @@ function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, Co
131172
defaultValue: metadata.defaultValue,
132173
defaultValueDescription: metadata.defaultValueDescription,
133174
});
134-
};
135-
136-
// Process all string options
137-
for (const key of options.string) {
138-
addEnvVar(key, "string");
139-
}
140-
141-
// Process all number options
142-
for (const key of options.number) {
143-
addEnvVar(key, "number");
144-
}
145-
146-
// Process all boolean options
147-
for (const key of options.boolean) {
148-
addEnvVar(key, "boolean");
149-
}
150-
151-
// Process all array options
152-
for (const key of options.array) {
153-
addEnvVar(key, "array");
154175
}
155176

156177
// Sort by name for consistent output
@@ -270,6 +291,9 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string {
270291
case "string":
271292
defaultValueString = `\`"${defaultValue}"\``;
272293
break;
294+
case "object":
295+
defaultValueString = `\`"${JSON.stringify(defaultValue)}"\``;
296+
break;
273297
default:
274298
throw new Error(`Unsupported default value type: ${typeof defaultValue}`);
275299
}
@@ -307,7 +331,7 @@ function updateReadmeConfigTable(envVars: ArgumentInfo[]): void {
307331
function main(): void {
308332
const zodMetadata = extractZodDescriptions();
309333

310-
const argumentInfo = getArgumentInfo(OPTIONS, zodMetadata);
334+
const argumentInfo = getArgumentInfo(zodMetadata);
311335
updateServerJsonEnvVars(argumentInfo);
312336
updateReadmeConfigTable(argumentInfo);
313337
}

0 commit comments

Comments
 (0)