Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
23 changes: 23 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ jobs:
RELEASE_CHANNEL: ${{ steps.npm-tag.outputs.RELEASE_CHANNEL }}
steps:
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
- uses: mongodb-js/devtools-shared/actions/setup-bot-token@main
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this in the wrong job?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is it? seems to be the way we get the token everywhere

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're adding this to the check job, but seems like you're trying to use it in the publish job. Step outputs cannot be passed across jobs.

id: app-token
with:
app-id: ${{ vars.DEVTOOLS_BOT_APP_ID }}
private-key: ${{ secrets.DEVTOOLS_BOT_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
fetch-depth: 0
Expand Down Expand Up @@ -75,6 +80,7 @@ jobs:
environment: Production
permissions:
contents: write
id-token: write # Required for OIDC authentication with MCP Registry
needs:
- check
if: needs.check.outputs.VERSION_EXISTS == 'false'
Expand All @@ -95,6 +101,23 @@ jobs:
run: npm publish --tag ${{ needs.check.outputs.RELEASE_CHANNEL }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Update server.json version and arguments
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we move this at least temporarily as the last step? I expect there may be rough edges in the first couple of runs, so don't want this failing to impact the other steps.

run: |
VERSION="${{ needs.check.outputs.VERSION }}"
VERSION="${VERSION#v}"
npm run generate:arguments

- 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

- name: Publish git release
env:
GH_TOKEN: ${{ github.token }}
Expand Down
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