Skip to content

Commit b6bfdfe

Browse files
authored
chore: add MCP registry publishing (#679)
1 parent 567d497 commit b6bfdfe

File tree

7 files changed

+1032
-35
lines changed

7 files changed

+1032
-35
lines changed

.github/workflows/prepare-release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ jobs:
3535
run: |
3636
echo "NEW_VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)" >> $GITHUB_OUTPUT
3737
npm run build:update-package-version
38+
39+
- name: Update server.json version and arguments
40+
run: |
41+
npm run generate:arguments
42+
3843
- name: Create release PR
3944
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # 7.0.8
4045
id: create-pr

.github/workflows/publish.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ jobs:
8080
if: needs.check.outputs.VERSION_EXISTS == 'false'
8181
steps:
8282
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
83+
- uses: mongodb-js/devtools-shared/actions/setup-bot-token@main
84+
id: app-token
85+
with:
86+
app-id: ${{ vars.DEVTOOLS_BOT_APP_ID }}
87+
private-key: ${{ secrets.DEVTOOLS_BOT_PRIVATE_KEY }}
8388
- uses: actions/checkout@v5
8489
- uses: actions/setup-node@v6
8590
with:
@@ -95,8 +100,19 @@ jobs:
95100
run: npm publish --tag ${{ needs.check.outputs.RELEASE_CHANNEL }}
96101
env:
97102
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
103+
98104
- name: Publish git release
99105
env:
100106
GH_TOKEN: ${{ github.token }}
101107
run: |
102108
gh release create ${{ needs.check.outputs.VERSION }} --title "${{ needs.check.outputs.VERSION }}" --generate-notes --target ${{ github.sha }} ${{ (needs.check.outputs.RELEASE_CHANNEL != 'latest' && '--prerelease') || ''}}
109+
110+
- name: Install MCP Publisher
111+
run: |
112+
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
113+
114+
- name: Login to MCP Registry
115+
run: ./mcp-publisher login github --token ${{ steps.app-token.outputs.token }}
116+
117+
- name: Publish to MCP Registry
118+
run: ./mcp-publisher publish

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ ENTRYPOINT ["mongodb-mcp-server"]
99
LABEL maintainer="MongoDB Inc <info@mongodb.com>"
1010
LABEL description="MongoDB MCP Server"
1111
LABEL version=${VERSION}
12+
LABEL io.modelcontextprotocol.server.name="io.github.mongodb-js/mongodb-mcp-server"

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"description": "MongoDB Model Context Protocol Server",
44
"version": "1.1.0",
55
"type": "module",
6+
"mcpName": "io.github.mongodb-js/mongodb-mcp-server",
67
"exports": {
78
".": {
89
"import": {
@@ -51,7 +52,8 @@
5152
"fix": "npm run fix:lint && npm run reformat",
5253
"fix:lint": "eslint . --fix",
5354
"reformat": "prettier --write .",
54-
"generate": "./scripts/generate.sh",
55+
"generate": "./scripts/generate.sh && npm run generate:arguments",
56+
"generate:arguments": "tsx scripts/generateArguments.ts",
5557
"test": "vitest --project eslint-rules --project unit-and-integration --coverage",
5658
"pretest:accuracy": "npm run build",
5759
"test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh",

scripts/generateArguments.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/usr/bin/env tsx
2+
3+
/**
4+
* This script generates argument definitions and updates:
5+
* - server.json arrays
6+
* - TODO: README.md configuration table
7+
*
8+
* It uses the Zod schema and OPTIONS defined in src/common/config.ts
9+
*/
10+
11+
import { readFileSync, writeFileSync } from "fs";
12+
import { join, dirname } from "path";
13+
import { fileURLToPath } from "url";
14+
import { OPTIONS, UserConfigSchema } from "../src/common/config.js";
15+
import type { ZodObject, ZodRawShape } from "zod";
16+
17+
const __filename = fileURLToPath(import.meta.url);
18+
const __dirname = dirname(__filename);
19+
20+
function camelCaseToSnakeCase(str: string): string {
21+
return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase();
22+
}
23+
24+
// List of configuration keys that contain sensitive/secret information
25+
// These should be redacted in logs and marked as secret in environment variable definitions
26+
const SECRET_CONFIG_KEYS = new Set([
27+
"connectionString",
28+
"username",
29+
"password",
30+
"apiClientId",
31+
"apiClientSecret",
32+
"tlsCAFile",
33+
"tlsCertificateKeyFile",
34+
"tlsCertificateKeyFilePassword",
35+
"tlsCRLFile",
36+
"sslCAFile",
37+
"sslPEMKeyFile",
38+
"sslPEMKeyPassword",
39+
"sslCRLFile",
40+
"voyageApiKey",
41+
]);
42+
43+
interface EnvironmentVariable {
44+
name: string;
45+
description: string;
46+
isRequired: boolean;
47+
format: string;
48+
isSecret: boolean;
49+
configKey: string;
50+
defaultValue?: unknown;
51+
}
52+
53+
interface ConfigMetadata {
54+
description: string;
55+
defaultValue?: unknown;
56+
}
57+
58+
function extractZodDescriptions(): Record<string, ConfigMetadata> {
59+
const result: Record<string, ConfigMetadata> = {};
60+
61+
// Get the shape of the Zod schema
62+
const shape = (UserConfigSchema as ZodObject<ZodRawShape>).shape;
63+
64+
for (const [key, fieldSchema] of Object.entries(shape)) {
65+
const schema = fieldSchema;
66+
// Extract description from Zod schema
67+
const description = schema.description || `Configuration option: ${key}`;
68+
69+
// Extract default value if present
70+
let defaultValue: unknown = undefined;
71+
if (schema._def && "defaultValue" in schema._def) {
72+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
73+
defaultValue = schema._def.defaultValue() as unknown;
74+
}
75+
76+
result[key] = {
77+
description,
78+
defaultValue,
79+
};
80+
}
81+
82+
return result;
83+
}
84+
85+
function generateEnvironmentVariables(
86+
options: typeof OPTIONS,
87+
zodMetadata: Record<string, ConfigMetadata>
88+
): EnvironmentVariable[] {
89+
const envVars: EnvironmentVariable[] = [];
90+
const processedKeys = new Set<string>();
91+
92+
// Helper to add env var
93+
const addEnvVar = (key: string, type: "string" | "number" | "boolean" | "array"): void => {
94+
if (processedKeys.has(key)) return;
95+
processedKeys.add(key);
96+
97+
const envVarName = `MDB_MCP_${camelCaseToSnakeCase(key)}`;
98+
99+
// Get description and default value from Zod metadata
100+
const metadata = zodMetadata[key] || {
101+
description: `Configuration option: ${key}`,
102+
};
103+
104+
// Determine format based on type
105+
let format = type;
106+
if (type === "array") {
107+
format = "string"; // Arrays are passed as comma-separated strings
108+
}
109+
110+
envVars.push({
111+
name: envVarName,
112+
description: metadata.description,
113+
isRequired: false,
114+
format: format,
115+
isSecret: SECRET_CONFIG_KEYS.has(key),
116+
configKey: key,
117+
defaultValue: metadata.defaultValue,
118+
});
119+
};
120+
121+
// Process all string options
122+
for (const key of options.string) {
123+
addEnvVar(key, "string");
124+
}
125+
126+
// Process all number options
127+
for (const key of options.number) {
128+
addEnvVar(key, "number");
129+
}
130+
131+
// Process all boolean options
132+
for (const key of options.boolean) {
133+
addEnvVar(key, "boolean");
134+
}
135+
136+
// Process all array options
137+
for (const key of options.array) {
138+
addEnvVar(key, "array");
139+
}
140+
141+
// Sort by name for consistent output
142+
return envVars.sort((a, b) => a.name.localeCompare(b.name));
143+
}
144+
145+
function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] {
146+
const packageArguments: unknown[] = [];
147+
148+
// Generate positional arguments from the same config options (only documented ones)
149+
const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:"));
150+
151+
// Generate named arguments from the same config options
152+
for (const argument of documentedVars) {
153+
const arg: Record<string, unknown> = {
154+
type: "named",
155+
name: "--" + argument.configKey,
156+
description: argument.description,
157+
isRequired: argument.isRequired,
158+
};
159+
160+
// Add format if it's not string (string is the default)
161+
if (argument.format !== "string") {
162+
arg.format = argument.format;
163+
}
164+
165+
packageArguments.push(arg);
166+
}
167+
168+
return packageArguments;
169+
}
170+
171+
function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
172+
const serverJsonPath = join(__dirname, "..", "server.json");
173+
const packageJsonPath = join(__dirname, "..", "package.json");
174+
175+
const content = readFileSync(serverJsonPath, "utf-8");
176+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version: string };
177+
const serverJson = JSON.parse(content) as {
178+
version?: string;
179+
packages: {
180+
registryType?: string;
181+
identifier?: string;
182+
environmentVariables: EnvironmentVariable[];
183+
packageArguments?: unknown[];
184+
version?: string;
185+
}[];
186+
};
187+
188+
// Get version from package.json
189+
const version = packageJson.version;
190+
191+
// Generate environment variables array (only documented ones)
192+
const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:"));
193+
const envVarsArray = documentedVars.map((v) => ({
194+
name: v.name,
195+
description: v.description,
196+
isRequired: v.isRequired,
197+
format: v.format,
198+
isSecret: v.isSecret,
199+
}));
200+
201+
// Generate package arguments (named arguments in camelCase)
202+
const packageArguments = generatePackageArguments(envVars);
203+
204+
// Update version at root level
205+
serverJson.version = process.env.VERSION || version;
206+
207+
// Update environmentVariables, packageArguments, and version for all packages
208+
if (serverJson.packages && Array.isArray(serverJson.packages)) {
209+
for (const pkg of serverJson.packages) {
210+
pkg.environmentVariables = envVarsArray as EnvironmentVariable[];
211+
pkg.packageArguments = packageArguments;
212+
pkg.version = version;
213+
214+
// Update OCI identifier version tag if this is an OCI package
215+
if (pkg.registryType === "oci" && pkg.identifier) {
216+
// Replace the version tag in the OCI identifier (e.g., docker.io/mongodb/mongodb-mcp-server:1.0.0)
217+
pkg.identifier = pkg.identifier.replace(/:[^:]+$/, `:${version}`);
218+
}
219+
}
220+
}
221+
222+
writeFileSync(serverJsonPath, JSON.stringify(serverJson, null, 2) + "\n", "utf-8");
223+
console.log(`✓ Updated server.json (version ${version})`);
224+
}
225+
226+
function main(): void {
227+
const zodMetadata = extractZodDescriptions();
228+
229+
const envVars = generateEnvironmentVariables(OPTIONS, zodMetadata);
230+
updateServerJsonEnvVars(envVars);
231+
}
232+
233+
main();

0 commit comments

Comments
 (0)