Skip to content

Commit cebb0ba

Browse files
committed
chore: add server.json file the MCP registry
1 parent 567d497 commit cebb0ba

File tree

6 files changed

+1096
-35
lines changed

6 files changed

+1096
-35
lines changed

.github/workflows/publish.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ jobs:
7575
environment: Production
7676
permissions:
7777
contents: write
78+
id-token: write # Required for OIDC authentication with MCP Registry
7879
needs:
7980
- check
8081
if: needs.check.outputs.VERSION_EXISTS == 'false'
@@ -95,6 +96,22 @@ jobs:
9596
run: npm publish --tag ${{ needs.check.outputs.RELEASE_CHANNEL }}
9697
env:
9798
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
99+
100+
- name: Update server.json version and arguments
101+
run: |
102+
VERSION="${{ needs.check.outputs.VERSION }}"
103+
VERSION="${VERSION#v}"
104+
npm run generate:arguments
105+
106+
- name: Install MCP Publisher
107+
run: brew install mcp-publisher
108+
109+
- name: Login to MCP Registry
110+
run: mcp-publisher login github-oidc
111+
112+
- name: Publish to MCP Registry
113+
run: mcp-publisher publish
114+
98115
- name: Publish git release
99116
env:
100117
GH_TOKEN: ${{ github.token }}

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 <[email protected]>"
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:args": "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: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
#!/usr/bin/env tsx
2+
3+
/**
4+
* This script generates environment variable definitions and updates:
5+
* - server.json environmentVariables 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 { 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 ParsedOptions {
44+
string: string[];
45+
number: string[];
46+
boolean: string[];
47+
array: string[];
48+
alias: Record<string, string>;
49+
}
50+
51+
interface EnvironmentVariable {
52+
name: string;
53+
description: string;
54+
isRequired: boolean;
55+
format: string;
56+
isSecret: boolean;
57+
configKey: string;
58+
defaultValue?: unknown;
59+
}
60+
61+
interface ConfigMetadata {
62+
description: string;
63+
defaultValue?: unknown;
64+
}
65+
66+
function extractZodDescriptions(): Record<string, ConfigMetadata> {
67+
const result: Record<string, ConfigMetadata> = {};
68+
69+
// Get the shape of the Zod schema
70+
const shape = (UserConfigSchema as ZodObject<ZodRawShape>).shape;
71+
72+
for (const [key, fieldSchema] of Object.entries(shape)) {
73+
const schema = fieldSchema;
74+
// Extract description from Zod schema
75+
const description = schema.description || `Configuration option: ${key}`;
76+
77+
// Extract default value if present
78+
let defaultValue: unknown = undefined;
79+
if (schema._def && "defaultValue" in schema._def) {
80+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
81+
defaultValue = schema._def.defaultValue() as unknown;
82+
}
83+
84+
result[key] = {
85+
description,
86+
defaultValue,
87+
};
88+
}
89+
90+
return result;
91+
}
92+
93+
function parseOptionsFromConfig(): ParsedOptions {
94+
const configPath = join(__dirname, "..", "src", "common", "config.ts");
95+
const configContent = readFileSync(configPath, "utf-8");
96+
97+
// Extract the OPTIONS object using regex
98+
const optionsMatch = configContent.match(/const OPTIONS = \{([\s\S]*?)\} as Readonly<Options>;/);
99+
100+
if (!optionsMatch) {
101+
throw new Error("Could not find OPTIONS object in config.ts");
102+
}
103+
104+
const optionsContent = optionsMatch[1];
105+
106+
// Parse each array type
107+
const parseArray = (type: string): string[] => {
108+
const regex = new RegExp(`${type}:\\s*\\[(.*?)\\]`, "s");
109+
const match = optionsContent?.match(regex);
110+
if (!match) return [];
111+
112+
// Extract quoted strings from the array
113+
const arrayContent = match[1];
114+
if (!arrayContent) return [];
115+
const items = arrayContent.match(/"([^"]+)"/g);
116+
return items ? items.map((item) => item.replace(/"/g, "")) : [];
117+
};
118+
119+
// Parse alias object
120+
const parseAlias = (): Record<string, string> => {
121+
const aliasMatch = optionsContent?.match(/alias:\s*\{([\s\S]*?)\}/);
122+
if (!aliasMatch) return {};
123+
124+
const aliasContent = aliasMatch[1];
125+
if (!aliasContent) return {};
126+
const entries = aliasContent.matchAll(/(\w+):\s*"([^"]+)"/g);
127+
const result: Record<string, string> = {};
128+
129+
for (const match of entries) {
130+
if (match && match[1] && match[2]) {
131+
result[match[1]] = match[2];
132+
}
133+
}
134+
135+
return result;
136+
};
137+
138+
return {
139+
string: parseArray("string"),
140+
number: parseArray("number"),
141+
boolean: parseArray("boolean"),
142+
array: parseArray("array"),
143+
alias: parseAlias(),
144+
};
145+
}
146+
147+
function generateEnvironmentVariables(
148+
options: ParsedOptions,
149+
zodMetadata: Record<string, ConfigMetadata>
150+
): EnvironmentVariable[] {
151+
const envVars: EnvironmentVariable[] = [];
152+
const processedKeys = new Set<string>();
153+
154+
// Helper to add env var
155+
const addEnvVar = (key: string, type: "string" | "number" | "boolean" | "array"): void => {
156+
if (processedKeys.has(key)) return;
157+
processedKeys.add(key);
158+
159+
const envVarName = `MDB_MCP_${camelCaseToSnakeCase(key)}`;
160+
161+
// Get description and default value from Zod metadata
162+
const metadata = zodMetadata[key] || {
163+
description: `Configuration option: ${key}`,
164+
};
165+
166+
// Determine format based on type
167+
let format = type;
168+
if (type === "array") {
169+
format = "string"; // Arrays are passed as comma-separated strings
170+
}
171+
172+
envVars.push({
173+
name: envVarName,
174+
description: metadata.description,
175+
isRequired: false,
176+
format: format,
177+
isSecret: SECRET_CONFIG_KEYS.has(key),
178+
configKey: key,
179+
defaultValue: metadata.defaultValue,
180+
});
181+
};
182+
183+
// Process all string options
184+
for (const key of options.string) {
185+
addEnvVar(key, "string");
186+
}
187+
188+
// Process all number options
189+
for (const key of options.number) {
190+
addEnvVar(key, "number");
191+
}
192+
193+
// Process all boolean options
194+
for (const key of options.boolean) {
195+
addEnvVar(key, "boolean");
196+
}
197+
198+
// Process all array options
199+
for (const key of options.array) {
200+
addEnvVar(key, "array");
201+
}
202+
203+
// Sort by name for consistent output
204+
return envVars.sort((a, b) => a.name.localeCompare(b.name));
205+
}
206+
207+
function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] {
208+
const packageArguments: unknown[] = [];
209+
210+
// Generate positional arguments from the same config options (only documented ones)
211+
const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:"));
212+
213+
for (const envVar of documentedVars) {
214+
const arg: Record<string, unknown> = {
215+
type: "positional",
216+
valueHint: envVar.configKey,
217+
description: envVar.description,
218+
isRequired: envVar.isRequired,
219+
};
220+
221+
// Add format if it's not string (string is the default)
222+
if (envVar.format !== "string") {
223+
arg.format = envVar.format;
224+
}
225+
226+
packageArguments.push(arg);
227+
}
228+
229+
return packageArguments;
230+
}
231+
232+
function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
233+
const serverJsonPath = join(__dirname, "..", "server.json");
234+
const packageJsonPath = join(__dirname, "..", "package.json");
235+
236+
const content = readFileSync(serverJsonPath, "utf-8");
237+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version: string };
238+
const serverJson = JSON.parse(content) as {
239+
version?: string;
240+
packages: { environmentVariables: EnvironmentVariable[]; packageArguments?: unknown[]; version?: string }[];
241+
};
242+
243+
// Get version from package.json
244+
const version = packageJson.version;
245+
246+
// Generate environment variables array (only documented ones)
247+
const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:"));
248+
const envVarsArray = documentedVars.map((v) => ({
249+
name: v.name,
250+
description: v.description,
251+
isRequired: v.isRequired,
252+
format: v.format,
253+
isSecret: v.isSecret,
254+
}));
255+
256+
// Generate package arguments (positional arguments in camelCase)
257+
const packageArguments = generatePackageArguments(envVars);
258+
259+
// Update version at root level
260+
serverJson.version = process.env.VERSION || version;
261+
262+
// Update environmentVariables, packageArguments, and version for all packages
263+
if (serverJson.packages && Array.isArray(serverJson.packages)) {
264+
for (const pkg of serverJson.packages) {
265+
pkg.environmentVariables = envVarsArray as EnvironmentVariable[];
266+
pkg.packageArguments = packageArguments;
267+
pkg.version = version;
268+
269+
// Update OCI identifier version tag if this is an OCI package
270+
if (pkg.registryType === "oci" && pkg.identifier) {
271+
// Replace the version tag in the OCI identifier (e.g., docker.io/mongodb/mongodb-mcp-server:1.0.0)
272+
pkg.identifier = pkg.identifier.replace(/:[^:]+$/, `:${version}`);
273+
}
274+
}
275+
}
276+
277+
writeFileSync(serverJsonPath, JSON.stringify(serverJson, null, 2) + "\n", "utf-8");
278+
console.log(`✓ Updated server.json (version ${version})`);
279+
}
280+
281+
function main(): void {
282+
const zodMetadata = extractZodDescriptions();
283+
const options = parseOptionsFromConfig();
284+
285+
const envVars = generateEnvironmentVariables(options, zodMetadata);
286+
updateServerJsonEnvVars(envVars);
287+
}
288+
289+
main();

0 commit comments

Comments
 (0)