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

Merged
merged 36 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 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
9555dcd
Merge branch 'main' of github.com:mongodb-js/mongodb-mcp-server into …
gagik Aug 7, 2025
4ea91c3
fix: update to match
gagik Aug 7, 2025
f7942de
fix: use conventional inheritance pattern
gagik Aug 7, 2025
cd0c393
fix: remove unnecessary cast
gagik Aug 7, 2025
be5c06b
fix: make reducapply public
gagik Aug 7, 2025
f7e5167
fix: remove event listeners bit
gagik Aug 7, 2025
0e28212
fix: bring back event listeners
gagik Aug 7, 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
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
22 changes: 18 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@
"name": "mongodb-mcp-server",
"description": "MongoDB Model Context Protocol Server",
"version": "0.1.3",
"main": "dist/index.js",
"type": "module",
"exports": {
".": {
"import": {
"types": "./dist/lib.d.ts",
"default": "./dist/lib.js"
},
"require": {
"types": "./dist/cjs/lib.d.ts",
"default": "./dist/cjs/lib.js"
}
}
},
"main": "cjs/index.js",
"author": "MongoDB <[email protected]>",
"homepage": "https://github.com/mongodb-js/mongodb-mcp-server",
"repository": {
Expand All @@ -14,13 +27,14 @@
"publishConfig": {
"access": "public"
},
"type": "module",
"scripts": {
"prepare": "npm run build",
"build:clean": "rm -rf dist",
"build:compile": "tsc --project tsconfig.build.json",
"build:update-package-info": "tsx scripts/update-package-info.ts",
"build:esm": "tsc --project tsconfig.esm.json && echo '{\"type\":\"module\"}' > dist/package.json",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Source: As you can also see that both our cjs and esm packages get a 1-line package.json file with a type property.

This is required because Node needs to know wether files with a .js extensions should be interpreted as CommonJS or ESM. It can’t figure it out after opening it.

Node will look at the nearest package.json to see if the type property was specified.

"build:cjs": "tsc --project tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
"build:chmod": "chmod +x dist/index.js",
"build": "npm run build:clean && npm run build:compile && npm run build:chmod",
"build": "npm run build:clean && npm run build:esm && npm run build:cjs && npm run build:chmod",
"inspect": "npm run build && mcp-inspector -- dist/index.js",
"prettier": "prettier",
"check": "npm run build && npm run check:types && npm run check:lint && npm run check:format",
Expand Down
26 changes: 26 additions & 0 deletions scripts/update-package-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node

import { readFileSync, writeFileSync } from "fs";
import { join } from "path";

interface PackageJson {
version: string;
name?: string;
description?: string;
}

// Read package.json
const packageJsonPath = join(import.meta.dirname, "..", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as PackageJson;

// Define the packageInfo.ts content
const packageInfoContent = `// This file was generated by scripts/update-package-info.ts - Do not edit it manually.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

there's no good way to have __dirname / import.meta.dirname as explored in https://evertpot.com/universal-commonjs-esm-typescript-packages/ and I couldn't think of anything better either. so instead we'll generate the file on-demand. This is similar to how it is done in github.com/mongodb-js/mongosh/

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);
26 changes: 26 additions & 0 deletions scripts/update-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node

import { readFileSync, writeFileSync } from "fs";
import { join } from "path";

interface PackageJson {
version: string;
name?: string;
description?: string;
}

// Read package.json
const packageJsonPath = join(import.meta.dirname, "..", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as PackageJson;

// Define the packageInfo.ts content
const packageInfoContent = `// This file was generated by scripts/update-version.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/update-package-info.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 { fileURLToPath } from "url";
import { describe, it, expect } from "vitest";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe("Build Test", () => {
it("should successfully require CommonJS module", () => {
const require = createRequire(__filename);
const cjsPath = path.resolve(__dirname, "../../dist/cjs/lib.js");

const cjsModule = require(cjsPath) as Record<string, unknown>;

Check failure on line 14 in tests/integration/build.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/build.test.ts > Build Test > should successfully require CommonJS module

Error: Invalid package config \\?\D:\a\mongodb-mcp-server\mongodb-mcp-server\dist\package.json. ❯ tests/integration/build.test.ts:14:27 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_INVALID_PACKAGE_CONFIG' }

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

it("should successfully import ESM module", async () => {
const esmPath = path.resolve(__dirname, "../../dist/lib.js");

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 cjsPath = path.resolve(__dirname, "../../dist/cjs/lib.js");
const cjsModule = require(cjsPath) as Record<string, unknown>;

Check failure on line 33 in tests/integration/build.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/build.test.ts > Build Test > should have matching exports between CommonJS and ESM modules

Error: Invalid package config \\?\D:\a\mongodb-mcp-server\mongodb-mcp-server\dist\package.json. ❯ tests/integration/build.test.ts:33:27 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_INVALID_PACKAGE_CONFIG' }

// Import ESM module
const esmPath = path.resolve(__dirname, "../../dist/lib.js");
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"
}
}
Loading