Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ coverage
# Generated assets by accuracy runs
.accuracy

.DS_Store
.DS_Store

# Development tool files
.yalc
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like yalc so would be great if this is gitgnored

yalc.lock
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,9 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
| `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | `120000` | Time in milliseconds between export cleanup cycles that remove expired export files. |
| `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | `300000` | Time in milliseconds after which an export is considered expired and eligible for cleanup. |
| `exportsPath` | `MDB_MCP_EXPORTS_PATH` | see below\* | Folder to store exported data files. |
| `httpHeaders` | `MDB_MCP_HTTP_HEADERS` | `"{}"` | Header that the HTTP server will validate when making requests (only used when transport is 'http'). |
| `httpHost` | `MDB_MCP_HTTP_HOST` | `"127.0.0.1"` | Host address to bind the HTTP server to (only used when transport is 'http'). |
| `httpPort` | `MDB_MCP_HTTP_PORT` | `3000` | Port number for the HTTP server (only used when transport is 'http'). |
| `httpPort` | `MDB_MCP_HTTP_PORT` | `3000` | Port number for the HTTP server (only used when transport is 'http'). Use 0 for a random port. |
| `idleTimeoutMs` | `MDB_MCP_IDLE_TIMEOUT_MS` | `600000` | Idle timeout for a client to disconnect (only applies to http transport). |
| `indexCheck` | `MDB_MCP_INDEX_CHECK` | `false` | When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan. |
| `logPath` | `MDB_MCP_LOG_PATH` | see below\* | Folder to store logs. |
Expand All @@ -371,6 +372,8 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow
| `readOnly` | `MDB_MCP_READ_ONLY` | `false` | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. |
| `telemetry` | `MDB_MCP_TELEMETRY` | `"enabled"` | When set to disabled, disables telemetry collection. |
| `transport` | `MDB_MCP_TRANSPORT` | `"stdio"` | Either 'stdio' or 'http'. |
| `vectorSearchDimensions` | `MDB_MCP_VECTOR_SEARCH_DIMENSIONS` | `1024` | Default number of dimensions for vector search embeddings. |
| `vectorSearchSimilarityFunction` | `MDB_MCP_VECTOR_SEARCH_SIMILARITY_FUNCTION` | `"euclidean"` | Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'. |
| `voyageApiKey` | `MDB_MCP_VOYAGE_API_KEY` | `""` | API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion). |

#### Logger Options
Expand Down
7 changes: 5 additions & 2 deletions eslint-rules/enforce-zod-v4.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import path from "path";

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

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

// Only allow zod v4 import in config.ts
if (currentFilePath === configFilePath) {
if (allowedFilePaths.includes(currentFilePath)) {
return {};
}

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@
"openapi-fetch": "^0.14.0",
"ts-levenshtein": "^1.0.7",
"voyage-ai-provider": "^2.0.0",
"yargs-parser": "21.1.1",
"zod": "^3.25.76"
},
"engines": {
Expand Down
7 changes: 5 additions & 2 deletions scripts/apply.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parseArgs } from "@mongosh/arg-parser/arg-parser";
import fs from "fs/promises";
import type { OpenAPIV3_1 } from "openapi-types";
import argv from "yargs-parser";
import z4 from "zod/v4";

function findObjectFromRef<T>(obj: T | OpenAPIV3_1.ReferenceObject, openapi: OpenAPIV3_1.Document): T {
const ref = (obj as OpenAPIV3_1.ReferenceObject).$ref;
Expand All @@ -23,7 +24,9 @@ function findObjectFromRef<T>(obj: T | OpenAPIV3_1.ReferenceObject, openapi: Ope
}

async function main(): Promise<void> {
const { spec, file } = argv(process.argv.slice(2));
const {
parsed: { spec, file },
} = parseArgs({ args: process.argv.slice(2), schema: z4.object({ spec: z4.string(), file: z4.string() }) });

if (!spec || !file) {
console.error("Please provide both --spec and --file arguments.");
Expand Down
102 changes: 63 additions & 39 deletions scripts/generateArguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
* - server.json arrays
* - README.md configuration table
*
* It uses the Zod schema and OPTIONS defined in src/common/config.ts
* It uses the UserConfig Zod Schema.
*/

import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { UserConfigSchema, configRegistry } from "../src/common/config/userConfig.js";
import { UserConfigSchemaWithCliOptions, configRegistry } from "../src/common/config/userConfig.js";
Copy link
Collaborator

@himanshusinghs himanshusinghs Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using the UserConfig schema together with CliOptions schema, yet, in the final server.json I don't see the entries from CliOptions schema? We should expect those right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmmm..... I guess in server.json we should; should we have any fields in the README?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's follow-up on this. otherwise we're going to dump a bunch of unrelated fields into it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea - I am also unsure if they deserve a place in server.json and README but worth noting that somehow with this script they don't end up there. Happy to address this as a follow up.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is existing behavior and I think the code is self-explanatory, let's follow up on this later

import { execSync } from "child_process";
import { OPTIONS } from "../src/common/config/argsParserOptions.js";
import type { z as z4 } from "zod/v4";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down Expand Up @@ -54,19 +54,69 @@ interface ConfigMetadata {
defaultValue?: unknown;
defaultValueDescription?: string;
isSecret?: boolean;
type: "string" | "number" | "boolean" | "array";
}

/**
* Derives the primitive type from a Zod schema by unwrapping wrappers like default, optional, preprocess, etc.
*/
function deriveZodType(schema: z4.ZodType): "string" | "number" | "boolean" | "array" {
const def = schema.def as unknown as Record<string, unknown>;
const typeName = def.type as string;

// Handle wrapped types (default, optional, nullable, etc.)
if (typeName === "default" || typeName === "optional" || typeName === "nullable") {
const innerType = def.innerType as z4.ZodType;
return deriveZodType(innerType);
}

// Handle preprocess - look at the schema being processed into
if (typeName === "pipe") {
const out = def.out as z4.ZodType;
return deriveZodType(out);
}

// Handle coerce wrapper
if (typeName === "coerce") {
const innerType = def.innerType as z4.ZodType;
return deriveZodType(innerType);
}

// Handle primitive types
if (typeName === "string" || typeName === "enum") {
return "string";
}
if (typeName === "number" || typeName === "int") {
return "number";
}
if (typeName === "boolean") {
return "boolean";
}
if (typeName === "array") {
return "array";
}
if (typeName === "object") {
// Objects are treated as strings (JSON strings)
return "string";
}

// Default fallback
return "string";
}

function extractZodDescriptions(): Record<string, ConfigMetadata> {
const result: Record<string, ConfigMetadata> = {};

// Get the shape of the Zod schema
const shape = UserConfigSchema.shape;
const shape = UserConfigSchemaWithCliOptions.shape;

for (const [key, fieldSchema] of Object.entries(shape)) {
const schema = fieldSchema;
// Extract description from Zod schema
let description = schema.description || `Configuration option: ${key}`;

const derivedType = deriveZodType(schema);

if ("innerType" in schema.def) {
// "pipe" is also used for our comma-separated arrays
if (schema.def.innerType.def.type === "pipe") {
Expand All @@ -93,31 +143,22 @@ function extractZodDescriptions(): Record<string, ConfigMetadata> {
defaultValue,
defaultValueDescription,
isSecret,
type: derivedType,
};
}

return result;
}

function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] {
function getArgumentInfo(zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] {
const argumentInfos: ArgumentInfo[] = [];
const processedKeys = new Set<string>();

// Helper to add env var
const addEnvVar = (key: string, type: "string" | "number" | "boolean" | "array"): void => {
if (processedKeys.has(key)) return;
processedKeys.add(key);

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

// Get description and default value from Zod metadata
const metadata = zodMetadata[key] || {
description: `Configuration option: ${key}`,
};

// Determine format based on type
let format = type;
if (type === "array") {
let format: string = metadata.type;
if (metadata.type === "array") {
format = "string"; // Arrays are passed as comma-separated strings
}

Expand All @@ -131,26 +172,6 @@ function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, Co
defaultValue: metadata.defaultValue,
defaultValueDescription: metadata.defaultValueDescription,
});
};

// Process all string options
for (const key of options.string) {
addEnvVar(key, "string");
}

// Process all number options
for (const key of options.number) {
addEnvVar(key, "number");
}

// Process all boolean options
for (const key of options.boolean) {
addEnvVar(key, "boolean");
}

// Process all array options
for (const key of options.array) {
addEnvVar(key, "array");
}

// Sort by name for consistent output
Expand Down Expand Up @@ -270,6 +291,9 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string {
case "string":
defaultValueString = `\`"${defaultValue}"\``;
break;
case "object":
defaultValueString = `\`${JSON.stringify(defaultValue)}\``;
break;
default:
throw new Error(`Unsupported default value type: ${typeof defaultValue}`);
}
Expand Down Expand Up @@ -307,7 +331,7 @@ function updateReadmeConfigTable(envVars: ArgumentInfo[]): void {
function main(): void {
const zodMetadata = extractZodDescriptions();

const argumentInfo = getArgumentInfo(OPTIONS, zodMetadata);
const argumentInfo = getArgumentInfo(zodMetadata);
updateServerJsonEnvVars(argumentInfo);
updateReadmeConfigTable(argumentInfo);
}
Expand Down
Loading