Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ 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'). 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). |
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: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default defineConfig([
"src/types/*.d.ts",
"tests/integration/fixtures/",
"eslint-rules",
".yalc",
]),
eslintPluginPrettierRecommended,
]);
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
"@modelcontextprotocol/sdk": "^1.24.2",
"@mongodb-js/device-id": "^0.3.1",
"@mongodb-js/devtools-proxy-support": "^0.5.3",
"@mongosh/arg-parser": "^3.19.0",
"@mongosh/arg-parser": "^3.23.0",
"@mongosh/service-provider-node-driver": "^3.17.5",
"ai": "^5.0.72",
"bson": "^6.10.4",
Expand All @@ -134,7 +134,6 @@
"openapi-fetch": "^0.15.0",
"ts-levenshtein": "^1.0.7",
"voyage-ai-provider": "^2.0.0",
"yargs-parser": "21.1.1",
"zod": "^3.25.76"
},
"engines": {
Expand Down
35 changes: 21 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 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,14 +24,16 @@ 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.");
process.exit(1);
}

const specFile = await fs.readFile(spec as string, "utf8");
const specFile = await fs.readFile(spec, "utf8");

const operations: {
path: string;
Expand Down Expand Up @@ -112,7 +115,7 @@ async ${methodName}(options${requiredParams ? "" : "?"}: FetchOptions<operations
})
.join("\n");

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

await fs.writeFile(file as string, output, "utf8");
await fs.writeFile(file, output, "utf8");
}

main().catch((error) => {
Expand Down
98 changes: 61 additions & 37 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 { 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,6 +54,54 @@ 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> {
Expand All @@ -67,6 +115,8 @@ function extractZodDescriptions(): Record<string, ConfigMetadata> {
// 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