diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 71a5b657..e3d2740b 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -55,4 +55,4 @@ jobs: rm -rf node_modules npm pkg set scripts.prepare="exit 0" npm install --omit=dev - - run: npx -y @modelcontextprotocol/inspector --cli --method tools/list -- node dist/index.js --connectionString "mongodb://localhost" + - run: npx -y @modelcontextprotocol/inspector --cli --method tools/list -- node dist/esm/index.js --connectionString "mongodb://localhost" diff --git a/.github/workflows/prepare_release.yaml b/.github/workflows/prepare_release.yaml index 5300316a..b5b9e372 100644 --- a/.github/workflows/prepare_release.yaml +++ b/.github/workflows/prepare_release.yaml @@ -32,6 +32,7 @@ jobs: id: bump-version run: | echo "NEW_VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)" >> $GITHUB_OUTPUT + npm run build:update-version - name: Create release PR uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # 7.0.8 id: create-pr diff --git a/.smithery/Dockerfile b/.smithery/Dockerfile index e2b469da..c518fbd3 100644 --- a/.smithery/Dockerfile +++ b/.smithery/Dockerfile @@ -27,4 +27,4 @@ RUN npm ci --production --ignore-scripts # Expose no ports (stdio only) # Default command -CMD ["node", "dist/index.js"] +CMD ["node", "dist/esm/index.js"] diff --git a/.smithery/smithery.yaml b/.smithery/smithery.yaml index 13952c7b..6e7f7eb7 100644 --- a/.smithery/smithery.yaml +++ b/.smithery/smithery.yaml @@ -40,7 +40,7 @@ startCommand: # A function that produces the CLI command to start the MCP on stdio. |- (config) => { - const args = ['dist/index.js']; + const args = ['dist/esm/index.js']; if (config) { if (config.atlasClientId) { args.push('--apiClientId'); diff --git a/.vscode/launch.json b/.vscode/launch.json index 0756e2d0..e4985d03 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,7 @@ "request": "launch", "name": "Launch Program", "skipFiles": ["/**"], - "program": "${workspaceFolder}/dist/index.js", + "program": "${workspaceFolder}/dist/esm/index.js", "preLaunchTask": "tsc: build - tsconfig.build.json", "outFiles": ["${workspaceFolder}/dist/**/*.js"] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b35e5f4b..34fe72f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ This project implements a Model Context Protocol (MCP) server for MongoDB and Mo { "mcpServers": { "MongoDB": { - "command": "/path/to/mongodb-mcp-server/dist/index.js" + "command": "/path/to/mongodb-mcp-server/dist/esm/index.js" } } } @@ -104,7 +104,7 @@ npm run inspect This is equivalent to: ```shell -npx @modelcontextprotocol/inspector -- node dist/index.js +npx @modelcontextprotocol/inspector -- node dist/esm/index.js ``` ## Pull Request Guidelines diff --git a/package-lock.json b/package-lock.json index 3d2543e8..7a2c7e42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "zod": "^3.25.76" }, "bin": { - "mongodb-mcp-server": "dist/index.js" + "mongodb-mcp-server": "dist/esm/index.js" }, "devDependencies": { "@eslint/js": "^9.30.1", diff --git a/package.json b/package.json index 805b6509..4b431713 100644 --- a/package.json +++ b/package.json @@ -2,26 +2,45 @@ "name": "mongodb-mcp-server", "description": "MongoDB Model Context Protocol Server", "version": "0.1.3", - "main": "dist/index.js", + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/esm/lib.d.ts", + "default": "./dist/esm/lib.js" + }, + "require": { + "types": "./dist/cjs/lib.d.ts", + "default": "./dist/cjs/lib.js" + } + } + }, + "main": "./dist/cjs/lib.js", + "types": "./dist/cjs/lib.d.ts", "author": "MongoDB ", "homepage": "https://github.com/mongodb-js/mongodb-mcp-server", "repository": { "url": "https://github.com/mongodb-js/mongodb-mcp-server.git" }, "bin": { - "mongodb-mcp-server": "dist/index.js" + "mongodb-mcp-server": "dist/esm/index.js" }, "publishConfig": { "access": "public" }, - "type": "module", + "files": [ + "dist" + ], "scripts": { "prepare": "npm run build", "build:clean": "rm -rf dist", - "build:compile": "tsc --project tsconfig.build.json", - "build:chmod": "chmod +x dist/index.js", - "build": "npm run build:clean && npm run build:compile && npm run build:chmod", - "inspect": "npm run build && mcp-inspector -- dist/index.js", + "build:update-package-version": "tsx scripts/updatePackageVersion.ts", + "build:esm": "tsc --project tsconfig.esm.json", + "build:cjs": "tsc --project tsconfig.cjs.json", + "build:universal-package": "tsx scripts/createUniversalPackage.ts", + "build:chmod": "chmod +x dist/esm/index.js", + "build": "npm run build:clean && npm run build:esm && npm run build:cjs && npm run build:universal-package && npm run build:chmod", + "inspect": "npm run build && mcp-inspector -- dist/esm/index.js", "prettier": "prettier", "check": "npm run build && npm run check:types && npm run check:lint && npm run check:format", "check:lint": "eslint .", diff --git a/scripts/createUniversalPackage.ts b/scripts/createUniversalPackage.ts new file mode 100644 index 00000000..1f8381bc --- /dev/null +++ b/scripts/createUniversalPackage.ts @@ -0,0 +1,25 @@ +#!/usr/bin/env tsx + +import { writeFileSync, mkdirSync } from "fs"; +import { resolve } from "path"; + +const distDir = resolve("dist"); + +/** + * Node uses the package.json to know whether files with a .js extensions + * should be interpreted as CommonJS or ESM. + */ +// ESM package.json +const esmPath = resolve(distDir, "esm", "package.json"); +mkdirSync(resolve(distDir, "esm"), { recursive: true }); +writeFileSync(esmPath, JSON.stringify({ type: "module" })); + +// CJS package.json +const cjsPath = resolve(distDir, "cjs", "package.json"); +mkdirSync(resolve(distDir, "cjs"), { recursive: true }); +writeFileSync(cjsPath, JSON.stringify({ type: "commonjs" })); + +// Create a dist/index.js file that imports the ESM index.js file +// To minimize breaking changes from pre-universal package time. +const indexPath = resolve(distDir, "index.js"); +writeFileSync(indexPath, `import "./esm/index.js";`); diff --git a/scripts/updatePackageVersion.ts b/scripts/updatePackageVersion.ts new file mode 100644 index 00000000..ddb1b315 --- /dev/null +++ b/scripts/updatePackageVersion.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from "fs"; +import { join } from "path"; + +// Read package.json +const packageJsonPath = join(import.meta.dirname, "..", "package.json"); +const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { + version: string; +}; + +// Define the packageInfo.ts content +const packageInfoContent = `// This file was generated by scripts/updatePackageVersion.ts - Do not edit it manually. +export const packageInfo = { + version: "${packageJson.version}", + mcpServerName: "MongoDB MCP Server", +}; +`; + +// Write to packageInfo.ts +const packageInfoPath = join(import.meta.dirname, "..", "src", "common", "packageInfo.ts"); +writeFileSync(packageInfoPath, packageInfoContent); diff --git a/src/common/packageInfo.ts b/src/common/packageInfo.ts index 6c075dc0..fc86dafe 100644 --- a/src/common/packageInfo.ts +++ b/src/common/packageInfo.ts @@ -1,6 +1,5 @@ -import packageJson from "../../package.json" with { type: "json" }; - +// This file was generated by scripts/updatePackageVersion.ts - Do not edit it manually. export const packageInfo = { - version: packageJson.version, + version: "0.1.3", mcpServerName: "MongoDB MCP Server", }; diff --git a/src/common/session.ts b/src/common/session.ts index dfae6ec9..6e4d81bb 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -2,7 +2,7 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver import { ApiClient, ApiClientCredentials } from "./atlas/apiClient.js"; import { Implementation } from "@modelcontextprotocol/sdk/types.js"; import logger, { LogId } from "./logger.js"; -import EventEmitter from "events"; +import { EventEmitter } from "events"; import { ConnectOptions } from "./config.js"; import { setAppNameParamIfMissing } from "../helpers/connectionOptions.js"; import { packageInfo } from "./packageInfo.js"; diff --git a/src/index.ts b/src/index.ts index f94c4371..06b8cb22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,50 +9,54 @@ import { packageInfo } from "./common/packageInfo.js"; import { Telemetry } from "./telemetry/telemetry.js"; import { createEJsonTransport } from "./helpers/EJsonTransport.js"; -try { - const session = new Session({ - apiBaseUrl: config.apiBaseUrl, - apiClientId: config.apiClientId, - apiClientSecret: config.apiClientSecret, - }); - const mcpServer = new McpServer({ - name: packageInfo.mcpServerName, - version: packageInfo.version, - }); - - const telemetry = Telemetry.create(session, config); - - const server = new Server({ - mcpServer, - session, - telemetry, - userConfig: config, - }); - - const transport = createEJsonTransport(); - - const shutdown = () => { - logger.info(LogId.serverCloseRequested, "server", `Server close requested`); - - server - .close() - .then(() => { - logger.info(LogId.serverClosed, "server", `Server closed successfully`); - process.exit(0); - }) - .catch((err: unknown) => { - const error = err instanceof Error ? err : new Error(String(err)); - logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error.message}`); - process.exit(1); - }); - }; - - process.once("SIGINT", shutdown); - process.once("SIGTERM", shutdown); - process.once("SIGQUIT", shutdown); - - await server.connect(transport); -} catch (error: unknown) { - logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`); - process.exit(1); +async function main() { + try { + const session = new Session({ + apiBaseUrl: config.apiBaseUrl, + apiClientId: config.apiClientId, + apiClientSecret: config.apiClientSecret, + }); + const mcpServer = new McpServer({ + name: packageInfo.mcpServerName, + version: packageInfo.version, + }); + + const telemetry = Telemetry.create(session, config); + + const server = new Server({ + mcpServer, + session, + telemetry, + userConfig: config, + }); + + const transport = createEJsonTransport(); + + const shutdown = () => { + logger.info(LogId.serverCloseRequested, "server", `Server close requested`); + + server + .close() + .then(() => { + logger.info(LogId.serverClosed, "server", `Server closed successfully`); + process.exit(0); + }) + .catch((err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)); + logger.error(LogId.serverCloseFailure, "server", `Error closing server: ${error.message}`); + process.exit(1); + }); + }; + + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); + process.once("SIGQUIT", shutdown); + + await server.connect(transport); + } catch (error: unknown) { + logger.emergency(LogId.serverStartFailure, "server", `Fatal error running server: ${error as string}`); + process.exit(1); + } } + +void main(); diff --git a/src/lib.ts b/src/lib.ts new file mode 100644 index 00000000..773933ff --- /dev/null +++ b/src/lib.ts @@ -0,0 +1,4 @@ +export { Server, type ServerOptions } from "./server.js"; +export { Telemetry } from "./telemetry/telemetry.js"; +export { Session, type SessionOptions } from "./common/session.js"; +export type { UserConfig, ConnectOptions } from "./common/config.js"; diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts new file mode 100644 index 00000000..7282efe4 --- /dev/null +++ b/tests/integration/build.test.ts @@ -0,0 +1,46 @@ +import { createRequire } from "module"; +import path from "path"; +import { describe, it, expect } from "vitest"; + +// Current directory where the test file is located +const currentDir = import.meta.dirname; + +// Get project root (go up from tests/integration to project root) +const projectRoot = path.resolve(currentDir, "../.."); + +const esmPath = path.resolve(projectRoot, "dist/esm/lib.js"); +const cjsPath = path.resolve(projectRoot, "dist/cjs/lib.js"); + +describe("Build Test", () => { + it("should successfully require CommonJS module", () => { + const require = createRequire(__filename); + + const cjsModule = require(cjsPath) as Record; + + expect(cjsModule).toBeDefined(); + expect(typeof cjsModule).toBe("object"); + }); + + it("should successfully import ESM module", async () => { + const esmModule = (await import(esmPath)) as Record; + + expect(esmModule).toBeDefined(); + expect(typeof esmModule).toBe("object"); + }); + + it("should have matching exports between CommonJS and ESM modules", async () => { + // Import CommonJS module + const require = createRequire(__filename); + const cjsModule = require(cjsPath) as Record; + + // Import ESM module + const esmModule = (await import(esmPath)) as Record; + + // Compare exports + const cjsKeys = Object.keys(cjsModule).sort(); + const esmKeys = Object.keys(esmModule).sort(); + + expect(cjsKeys).toEqual(esmKeys); + expect(cjsKeys).toEqual(["Server", "Session", "Telemetry"]); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 57edf983..bb6843fa 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -14,7 +14,9 @@ "skipLibCheck": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, - "typeRoots": ["./node_modules/@types", "./src/types"] + "typeRoots": ["./node_modules/@types", "./src/types"], + "declaration": true, + "declarationMap": true }, "include": ["src/**/*.ts"] } diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 00000000..ad8b3832 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "outDir": "./dist/cjs" + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 00000000..d1ba80d1 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "./dist/esm" + } +}