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
5 changes: 5 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ jobs:
run: |
echo "NEW_VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)" >> $GITHUB_OUTPUT
npm run build:update-package-version

- name: Update server.json version and arguments
run: |
npm run generate:arguments

- name: Create release PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # 7.0.8
id: create-pr
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ jobs:
if: needs.check.outputs.VERSION_EXISTS == 'false'
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: mongodb-js/devtools-shared/actions/setup-bot-token@main
id: app-token
with:
app-id: ${{ vars.DEVTOOLS_BOT_APP_ID }}
private-key: ${{ secrets.DEVTOOLS_BOT_PRIVATE_KEY }}
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
Expand All @@ -95,8 +100,19 @@ jobs:
run: npm publish --tag ${{ needs.check.outputs.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish git release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create ${{ needs.check.outputs.VERSION }} --title "${{ needs.check.outputs.VERSION }}" --generate-notes --target ${{ github.sha }} ${{ (needs.check.outputs.RELEASE_CHANNEL != 'latest' && '--prerelease') || ''}}

- name: Install MCP Publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher

- name: Login to MCP Registry
run: ./mcp-publisher login github --token ${{ steps.app-token.outputs.token }}

- name: Publish to MCP Registry
run: ./mcp-publisher publish
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ ENTRYPOINT ["mongodb-mcp-server"]
LABEL maintainer="MongoDB Inc <info@mongodb.com>"
LABEL description="MongoDB MCP Server"
LABEL version=${VERSION}
LABEL io.modelcontextprotocol.server.name="io.github.mongodb-js/mongodb-mcp-server"
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "MongoDB Model Context Protocol Server",
"version": "1.1.0",
"type": "module",
"mcpName": "io.github.mongodb-js/mongodb-mcp-server",
"exports": {
".": {
"import": {
Expand Down Expand Up @@ -51,7 +52,8 @@
"fix": "npm run fix:lint && npm run reformat",
"fix:lint": "eslint . --fix",
"reformat": "prettier --write .",
"generate": "./scripts/generate.sh",
"generate": "./scripts/generate.sh && npm run generate:arguments",
"generate:arguments": "tsx scripts/generateArguments.ts",
"test": "vitest --project eslint-rules --project unit-and-integration --coverage",
"pretest:accuracy": "npm run build",
"test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh",
Expand Down
233 changes: 233 additions & 0 deletions scripts/generateArguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#!/usr/bin/env tsx

/**
* This script generates argument definitions and updates:
* - server.json arrays
* - TODO: README.md configuration table
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 needed some extra handling but will be great to have in the future.

*
* It uses the Zod schema and OPTIONS defined in src/common/config.ts
*/

import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { OPTIONS, UserConfigSchema } from "../src/common/config.js";
import type { ZodObject, ZodRawShape } from "zod";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

function camelCaseToSnakeCase(str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase();
}

// List of configuration keys that contain sensitive/secret information
// These should be redacted in logs and marked as secret in environment variable definitions
const SECRET_CONFIG_KEYS = new Set([
"connectionString",
"username",
"password",
"apiClientId",
"apiClientSecret",
"tlsCAFile",
"tlsCertificateKeyFile",
"tlsCertificateKeyFilePassword",
"tlsCRLFile",
"sslCAFile",
"sslPEMKeyFile",
"sslPEMKeyPassword",
"sslCRLFile",
"voyageApiKey",
]);

interface EnvironmentVariable {
name: string;
description: string;
isRequired: boolean;
format: string;
isSecret: boolean;
configKey: string;
defaultValue?: unknown;
}

interface ConfigMetadata {
description: string;
defaultValue?: unknown;
}

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

// Get the shape of the Zod schema
const shape = (UserConfigSchema as ZodObject<ZodRawShape>).shape;

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

// Extract default value if present
let defaultValue: unknown = undefined;
if (schema._def && "defaultValue" in schema._def) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
defaultValue = schema._def.defaultValue() as unknown;
}

result[key] = {
description,
defaultValue,
};
}

return result;
}

function generateEnvironmentVariables(
options: typeof OPTIONS,
zodMetadata: Record<string, ConfigMetadata>
): EnvironmentVariable[] {
const envVars: EnvironmentVariable[] = [];
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);

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") {
format = "string"; // Arrays are passed as comma-separated strings
}

envVars.push({
name: envVarName,
description: metadata.description,
isRequired: false,
format: format,
isSecret: SECRET_CONFIG_KEYS.has(key),
configKey: key,
defaultValue: metadata.defaultValue,
});
};

// 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
return envVars.sort((a, b) => a.name.localeCompare(b.name));
}

function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] {
const packageArguments: unknown[] = [];

// Generate positional arguments from the same config options (only documented ones)
const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:"));

// Generate named arguments from the same config options
for (const argument of documentedVars) {
const arg: Record<string, unknown> = {
type: "named",
name: "--" + argument.configKey,
description: argument.description,
isRequired: argument.isRequired,
};

// Add format if it's not string (string is the default)
if (argument.format !== "string") {
arg.format = argument.format;
}

packageArguments.push(arg);
}

return packageArguments;
}

function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
const serverJsonPath = join(__dirname, "..", "server.json");
const packageJsonPath = join(__dirname, "..", "package.json");

const content = readFileSync(serverJsonPath, "utf-8");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version: string };
const serverJson = JSON.parse(content) as {
version?: string;
packages: {
registryType?: string;
identifier?: string;
environmentVariables: EnvironmentVariable[];
packageArguments?: unknown[];
version?: string;
}[];
};

// Get version from package.json
const version = packageJson.version;

// Generate environment variables array (only documented ones)
const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:"));
const envVarsArray = documentedVars.map((v) => ({
name: v.name,
description: v.description,
isRequired: v.isRequired,
format: v.format,
isSecret: v.isSecret,
}));

// Generate package arguments (named arguments in camelCase)
const packageArguments = generatePackageArguments(envVars);

// Update version at root level
serverJson.version = process.env.VERSION || version;

// Update environmentVariables, packageArguments, and version for all packages
if (serverJson.packages && Array.isArray(serverJson.packages)) {
for (const pkg of serverJson.packages) {
pkg.environmentVariables = envVarsArray as EnvironmentVariable[];
pkg.packageArguments = packageArguments;
pkg.version = version;

// Update OCI identifier version tag if this is an OCI package
if (pkg.registryType === "oci" && pkg.identifier) {
// Replace the version tag in the OCI identifier (e.g., docker.io/mongodb/mongodb-mcp-server:1.0.0)
pkg.identifier = pkg.identifier.replace(/:[^:]+$/, `:${version}`);
}
}
}

writeFileSync(serverJsonPath, JSON.stringify(serverJson, null, 2) + "\n", "utf-8");
console.log(`✓ Updated server.json (version ${version})`);
}

function main(): void {
const zodMetadata = extractZodDescriptions();

const envVars = generateEnvironmentVariables(OPTIONS, zodMetadata);
updateServerJsonEnvVars(envVars);
}

main();
Loading
Loading