Skip to content

chore(build): build a universal ESM and CommonJS package #371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6eed483
chore(tests): switch to vitest
gagik Jul 14, 2025
463b6cc
fix: remove traces of jest
gagik Jul 14, 2025
3b03327
fix: add and fix vitest linting
gagik Jul 14, 2025
2d63956
fix: coverage
gagik Jul 14, 2025
6e24d64
fix: use --exclude
gagik Jul 14, 2025
3f10cbd
fix: increase hook timeout
gagik Jul 14, 2025
52ccf24
fix: from feedback
gagik Jul 15, 2025
cbebf3e
feat: use custom toIncludeSameMembers matcher
gagik Jul 15, 2025
8fc12bc
Merge branch 'main' of github.com:mongodb-js/mongodb-mcp-server into …
gagik Jul 15, 2025
3bf00e6
fix: remove unneeded globals
gagik Jul 15, 2025
c138c1e
WIP:
gagik Jul 15, 2025
2e76d9a
wip
gagik Jul 15, 2025
49dccbe
fix: add test, keep backwards compatibility
gagik Jul 15, 2025
a262914
Merge branch 'main' of github.com:mongodb-js/mongodb-mcp-server into …
gagik Jul 15, 2025
5a12fec
fix: update package-lock
gagik Jul 15, 2025
ad51a41
fix: less scripts
gagik Jul 15, 2025
7064ad0
fix: clearer typescript
gagik Jul 15, 2025
70cd5c9
fix: add dist/cjs
gagik Jul 15, 2025
00a1b19
fix: conver to proper file URLs
gagik Jul 15, 2025
515b7c5
fix: use arelative
gagik Jul 15, 2025
6f5540e
fix: use project root
gagik Jul 15, 2025
c61e643
fix: use esm/lib.js
gagik Jul 15, 2025
9e4d91b
fix: resolve directly
gagik Jul 15, 2025
bf35972
fix: use a script to ensure windows support
gagik Jul 15, 2025
c70853e
fix: use esm as default
gagik Jul 15, 2025
384a3be
fix: remove redundant bits
gagik Jul 16, 2025
6a70bbe
fix: add backwards compatibility
gagik Jul 16, 2025
820292c
fix: add types
gagik Jul 16, 2025
cce6ddb
chore: package only dist and other req files
himanshusinghs Jun 25, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm setting to ESM to keep the changes minimal. Doing cjs default won't change our issues with the oidc-plugin, I think the only way to deal with that is to make some changes on the plugin itself to not depend on the require of an ESM module which is a higher Node version feature.

Generally worth checking if we're going to have any problems with dist/index.js going away.

I'd expect the worst being our dev environment configurations no longer being correct.

Otherwise we can also create a index.js alias to point to esm.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is solved, now that you have dist/index.js pointing to esm.

- run: npx -y @modelcontextprotocol/inspector --cli --method tools/list -- node dist/esm/index.js --connectionString "mongodb://localhost"
1 change: 1 addition & 0 deletions .github/workflows/prepare_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .smithery/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion .smithery/smithery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/index.js",
"program": "${workspaceFolder}/dist/esm/index.js",
"preLaunchTask": "tsc: build - tsconfig.build.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 26 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps we should add the types as well pointing to "./dist/cjs/lib.d.ts" considering that the main is for older tooling?

"types": "./dist/cjs/lib.d.ts",
"author": "MongoDB <[email protected]>",
"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 .",
Expand Down
25 changes: 25 additions & 0 deletions scripts/createUniversalPackage.ts
Original file line number Diff line number Diff line change
@@ -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");
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 is just out of precaution in case some of our deployments end up looking for dist/index.js... can be removed though

Copy link
Collaborator

Choose a reason for hiding this comment

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

I personally think its fine to have it.

writeFileSync(indexPath, `import "./esm/index.js";`);
22 changes: 22 additions & 0 deletions scripts/updatePackageVersion.ts
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 2 additions & 3 deletions src/common/packageInfo.ts
Original file line number Diff line number Diff line change
@@ -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",
};
2 changes: 1 addition & 1 deletion src/common/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
96 changes: 50 additions & 46 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
4 changes: 4 additions & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Server, type ServerOptions } from "./server.js";
export { Telemetry } from "./telemetry/telemetry.js";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we can consider exporting a single class which would make the setup easier but I'll leave that to future followups

export { Session, type SessionOptions } from "./common/session.js";
export type { UserConfig, ConnectOptions } from "./common/config.js";
46 changes: 46 additions & 0 deletions tests/integration/build.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

expect(cjsModule).toBeDefined();
expect(typeof cjsModule).toBe("object");
});

it("should successfully import ESM module", async () => {
const esmModule = (await import(esmPath)) as Record<string, unknown>;

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<string, unknown>;

// Import ESM module
const esmModule = (await import(esmPath)) as Record<string, unknown>;

// Compare exports
const cjsKeys = Object.keys(cjsModule).sort();
const esmKeys = Object.keys(esmModule).sort();

expect(cjsKeys).toEqual(esmKeys);
expect(cjsKeys).toEqual(["Server", "Session", "Telemetry"]);
});
});
4 changes: 3 additions & 1 deletion tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
8 changes: 8 additions & 0 deletions tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist/cjs"
}
}
8 changes: 8 additions & 0 deletions tsconfig.esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "./dist/esm"
}
}
Loading