Skip to content

Commit b0753c4

Browse files
committed
Merge branch 'main' into fmenezes/refactor_test
2 parents 773d10a + 064ca88 commit b0753c4

File tree

15 files changed

+494
-21
lines changed

15 files changed

+494
-21
lines changed

.github/workflows/code_health.yaml

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ jobs:
3131
cache: "npm"
3232
- name: Install dependencies
3333
run: npm ci
34-
- name: Run style check
35-
run: npm run generate
34+
- run: npm run generate
3635

3736
run-tests:
3837
strategy:
@@ -44,6 +43,26 @@ jobs:
4443
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
4544
if: matrix.os != 'windows-latest'
4645
- uses: actions/checkout@v4
46+
- uses: actions/setup-node@v4
47+
with:
48+
node-version-file: package.json
49+
cache: "npm"
50+
- name: Install dependencies
51+
run: npm ci
52+
- name: Run tests
53+
run: npm test
54+
- name: Upload test results
55+
if: always() && matrix.os == 'ubuntu-latest'
56+
uses: actions/upload-artifact@v4
57+
with:
58+
name: test-results
59+
path: coverage/lcov.info
60+
61+
run-atlas-tests:
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
65+
- uses: actions/checkout@v4
4766
- uses: actions/setup-node@v4
4867
with:
4968
node-version-file: package.json
@@ -55,10 +74,40 @@ jobs:
5574
MDB_MCP_API_CLIENT_ID: ${{ secrets.TEST_ATLAS_CLIENT_ID }}
5675
MDB_MCP_API_CLIENT_SECRET: ${{ secrets.TEST_ATLAS_CLIENT_SECRET }}
5776
MDB_MCP_API_BASE_URL: ${{ vars.TEST_ATLAS_BASE_URL }}
58-
run: npm test
77+
run: npm test -- --testPathIgnorePatterns "tests/integration/tools/mongodb" --testPathIgnorePatterns "tests/integration/[^/]+\.ts"
78+
- name: Upload test results
79+
uses: actions/upload-artifact@v4
80+
if: always()
81+
with:
82+
name: atlas-test-results
83+
path: coverage/lcov.info
84+
coverage:
85+
runs-on: ubuntu-latest
86+
needs: [run-tests, run-atlas-tests]
87+
if: always()
88+
steps:
89+
- uses: actions/checkout@v4
90+
- uses: actions/setup-node@v4
91+
with:
92+
node-version-file: package.json
93+
cache: "npm"
94+
- name: Install dependencies
95+
run: npm ci
96+
- name: Download test results
97+
uses: actions/download-artifact@v4
98+
with:
99+
name: test-results
100+
path: coverage/mongodb
101+
- name: Download atlas test results
102+
uses: actions/download-artifact@v4
103+
with:
104+
name: atlas-test-results
105+
path: coverage/atlas
106+
- name: Merge coverage reports
107+
run: |
108+
npx -y lcov-result-merger "coverage/*/lcov.info" "coverage/lcov.info"
59109
- name: Coveralls GitHub Action
60110
uses: coverallsapp/github-action@v2.3.6
61-
if: matrix.os == 'ubuntu-latest'
62111
with:
63112
file: coverage/lcov.info
64113
git-branch: ${{ github.head_ref || github.ref_name }}

package-lock.json

Lines changed: 30 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"jest-environment-node": "^29.7.0",
4848
"jest-extended": "^4.0.2",
4949
"mongodb-runner": "^5.8.2",
50+
"native-machine-id": "^0.0.8",
5051
"openapi-types": "^12.1.3",
5152
"openapi-typescript": "^7.6.1",
5253
"prettier": "^3.5.3",
@@ -61,6 +62,7 @@
6162
"@mongodb-js/devtools-connect": "^3.7.2",
6263
"@mongosh/service-provider-node-driver": "^3.6.0",
6364
"bson": "^6.10.3",
65+
"lru-cache": "^11.1.0",
6466
"mongodb": "^6.15.0",
6567
"mongodb-log-writer": "^2.4.1",
6668
"mongodb-redact": "^1.1.6",

src/common/atlas/apiClient.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
1+
import createClient, { Client, Middleware } from "openapi-fetch";
2+
import type { FetchOptions } from "openapi-fetch";
23
import { AccessToken, ClientCredentials } from "simple-oauth2";
34
import { ApiClientError } from "./apiClientError.js";
45
import { paths, operations } from "./openapi.js";
6+
import { BaseEvent } from "../../telemetry/types.js";
7+
import { mongoLogId } from "mongodb-log-writer";
8+
import logger from "../../logger.js";
59
import { packageInfo } from "../../packageInfo.js";
610

711
const ATLAS_API_VERSION = "2025-03-12";
@@ -93,6 +97,15 @@ export class ApiClient {
9397
this.client.use(this.errorMiddleware);
9498
}
9599

100+
public hasCredentials(): boolean {
101+
logger.info(
102+
mongoLogId(1_000_000),
103+
"api-client",
104+
`Checking if API client has credentials: ${!!(this.oauth2Client && this.accessToken)}`
105+
);
106+
return !!(this.oauth2Client && this.accessToken);
107+
}
108+
96109
public async getIpInfo(): Promise<{
97110
currentIpv4Address: string;
98111
}> {
@@ -118,6 +131,32 @@ export class ApiClient {
118131
}>;
119132
}
120133

134+
async sendEvents(events: BaseEvent[]): Promise<void> {
135+
let endpoint = "api/private/unauth/telemetry/events";
136+
const headers: Record<string, string> = {
137+
Accept: "application/json",
138+
"Content-Type": "application/json",
139+
"User-Agent": this.options.userAgent,
140+
};
141+
142+
const accessToken = await this.getAccessToken();
143+
if (accessToken) {
144+
endpoint = "api/private/v1.0/telemetry/events";
145+
headers["Authorization"] = `Bearer ${accessToken}`;
146+
}
147+
148+
const url = new URL(endpoint, this.options.baseUrl);
149+
const response = await fetch(url, {
150+
method: "POST",
151+
headers,
152+
body: JSON.stringify(events),
153+
});
154+
155+
if (!response.ok) {
156+
throw await ApiClientError.fromResponse(response);
157+
}
158+
}
159+
121160
// DO NOT EDIT. This is auto-generated code.
122161
async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
123162
const { data } = await this.client.GET("/api/atlas/v2/clusters", options);

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface UserConfig {
1010
apiBaseUrl?: string;
1111
apiClientId?: string;
1212
apiClientSecret?: string;
13+
telemetry?: "enabled" | "disabled";
1314
logPath: string;
1415
connectionString?: string;
1516
connectOptions: {

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,5 @@ try {
3030
await server.connect(transport);
3131
} catch (error: unknown) {
3232
logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error as string}`);
33-
3433
process.exit(1);
3534
}

src/server.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { AtlasTools } from "./tools/atlas/tools.js";
55
import { MongoDbTools } from "./tools/mongodb/tools.js";
66
import logger, { initializeLogger } from "./logger.js";
77
import { mongoLogId } from "mongodb-log-writer";
8+
import { ObjectId } from "mongodb";
9+
import { Telemetry } from "./telemetry/telemetry.js";
810
import { UserConfig } from "./config.js";
911

1012
export interface ServerOptions {
@@ -16,25 +18,35 @@ export interface ServerOptions {
1618
export class Server {
1719
public readonly session: Session;
1820
private readonly mcpServer: McpServer;
21+
private readonly telemetry: Telemetry;
1922
private readonly userConfig: UserConfig;
2023

2124
constructor({ session, mcpServer, userConfig }: ServerOptions) {
2225
this.session = session;
26+
this.telemetry = new Telemetry(session);
2327
this.mcpServer = mcpServer;
2428
this.userConfig = userConfig;
2529
}
2630

2731
async connect(transport: Transport) {
2832
this.mcpServer.server.registerCapabilities({ logging: {} });
29-
3033
this.registerTools();
3134
this.registerResources();
3235

3336
await initializeLogger(this.mcpServer, this.userConfig.logPath);
3437

3538
await this.mcpServer.connect(transport);
3639

37-
logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`);
40+
this.mcpServer.server.oninitialized = () => {
41+
this.session.setAgentRunner(this.mcpServer.server.getClientVersion());
42+
this.session.sessionId = new ObjectId().toString();
43+
44+
logger.info(
45+
mongoLogId(1_000_004),
46+
"server",
47+
`Server started with transport ${transport.constructor.name} and agent runner ${this.session.agentRunner?.name}`
48+
);
49+
};
3850
}
3951

4052
async close(): Promise<void> {
@@ -44,7 +56,7 @@ export class Server {
4456

4557
private registerTools() {
4658
for (const tool of [...AtlasTools, ...MongoDbTools]) {
47-
new tool(this.session, this.userConfig).register(this.mcpServer);
59+
new tool(this.session, this.userConfig, this.telemetry).register(this.mcpServer);
4860
}
4961
}
5062

src/session.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
22
import { ApiClient, ApiClientCredentials } from "./common/atlas/apiClient.js";
3+
import { Implementation } from "@modelcontextprotocol/sdk/types.js";
34

45
export interface SessionOptions {
56
apiBaseUrl?: string;
@@ -8,8 +9,13 @@ export interface SessionOptions {
89
}
910

1011
export class Session {
12+
sessionId?: string;
1113
serviceProvider?: NodeDriverServiceProvider;
1214
apiClient: ApiClient;
15+
agentRunner?: {
16+
name: string;
17+
version: string;
18+
};
1319

1420
constructor({ apiBaseUrl, apiClientId, apiClientSecret }: SessionOptions = {}) {
1521
const credentials: ApiClientCredentials | undefined =
@@ -26,6 +32,15 @@ export class Session {
2632
});
2733
}
2834

35+
setAgentRunner(agentRunner: Implementation | undefined) {
36+
if (agentRunner?.name && agentRunner?.version) {
37+
this.agentRunner = {
38+
name: agentRunner.name,
39+
version: agentRunner.version,
40+
};
41+
}
42+
}
43+
2944
async close(): Promise<void> {
3045
if (this.serviceProvider) {
3146
try {

src/telemetry/constants.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getMachineIdSync } from "native-machine-id";
2+
import { packageInfo } from "../packageInfo.js";
3+
4+
/**
5+
* Machine-specific metadata formatted for telemetry
6+
*/
7+
export const MACHINE_METADATA = {
8+
device_id: getMachineIdSync(),
9+
mcp_server_version: packageInfo.version,
10+
mcp_server_name: packageInfo.mcpServerName,
11+
platform: process.platform,
12+
arch: process.arch,
13+
os_type: process.platform,
14+
os_version: process.version,
15+
} as const;

0 commit comments

Comments
 (0)