mongodb-mcp-server 0.0.8 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
- package/.github/workflows/{lint.yml → check.yml} +22 -1
- package/.github/workflows/code_health.yaml +0 -22
- package/.github/workflows/code_health_fork.yaml +7 -63
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +11 -0
- package/README.md +41 -22
- package/dist/common/atlas/apiClient.js +141 -34
- package/dist/common/atlas/apiClient.js.map +1 -1
- package/dist/common/atlas/apiClientError.js +38 -5
- package/dist/common/atlas/apiClientError.js.map +1 -1
- package/dist/common/atlas/cluster.js +66 -0
- package/dist/common/atlas/cluster.js.map +1 -0
- package/dist/common/atlas/generatePassword.js +9 -0
- package/dist/common/atlas/generatePassword.js.map +1 -0
- package/dist/helpers/EJsonTransport.js +38 -0
- package/dist/helpers/EJsonTransport.js.map +1 -0
- package/dist/helpers/connectionOptions.js +10 -0
- package/dist/helpers/connectionOptions.js.map +1 -0
- package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
- package/dist/helpers/packageInfo.js.map +1 -0
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.js +2 -0
- package/dist/logger.js.map +1 -1
- package/dist/server.js +15 -11
- package/dist/server.js.map +1 -1
- package/dist/session.js +8 -3
- package/dist/session.js.map +1 -1
- package/dist/telemetry/constants.js +1 -3
- package/dist/telemetry/constants.js.map +1 -1
- package/dist/telemetry/eventCache.js.map +1 -1
- package/dist/telemetry/telemetry.js +46 -4
- package/dist/telemetry/telemetry.js.map +1 -1
- package/dist/tools/atlas/atlasTool.js +38 -0
- package/dist/tools/atlas/atlasTool.js.map +1 -1
- package/dist/tools/atlas/create/createDBUser.js +19 -2
- package/dist/tools/atlas/create/createDBUser.js.map +1 -1
- package/dist/tools/atlas/metadata/connectCluster.js +5 -22
- package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
- package/dist/tools/atlas/read/inspectCluster.js +4 -24
- package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
- package/dist/tools/atlas/read/listClusters.js +9 -18
- package/dist/tools/atlas/read/listClusters.js.map +1 -1
- package/dist/tools/mongodb/tools.js +2 -4
- package/dist/tools/mongodb/tools.js.map +1 -1
- package/eslint.config.js +2 -1
- package/{jest.config.ts → jest.config.cjs} +1 -1
- package/package.json +4 -2
- package/scripts/apply.ts +4 -1
- package/scripts/filter.ts +4 -0
- package/src/common/atlas/apiClient.ts +179 -37
- package/src/common/atlas/apiClientError.ts +58 -7
- package/src/common/atlas/cluster.ts +95 -0
- package/src/common/atlas/generatePassword.ts +10 -0
- package/src/common/atlas/openapi.d.ts +438 -15
- package/src/helpers/EJsonTransport.ts +47 -0
- package/src/helpers/connectionOptions.ts +20 -0
- package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
- package/src/index.ts +7 -3
- package/src/logger.ts +2 -0
- package/src/server.ts +22 -14
- package/src/session.ts +8 -4
- package/src/telemetry/constants.ts +2 -3
- package/src/telemetry/eventCache.ts +1 -1
- package/src/telemetry/telemetry.ts +72 -6
- package/src/telemetry/types.ts +0 -1
- package/src/tools/atlas/atlasTool.ts +47 -1
- package/src/tools/atlas/create/createDBUser.ts +22 -2
- package/src/tools/atlas/metadata/connectCluster.ts +5 -27
- package/src/tools/atlas/read/inspectCluster.ts +4 -40
- package/src/tools/atlas/read/listClusters.ts +19 -36
- package/src/tools/mongodb/tools.ts +2 -4
- package/src/types/mongodb-connection-string-url.d.ts +69 -0
- package/tests/integration/helpers.ts +18 -2
- package/tests/integration/telemetry.test.ts +28 -0
- package/tests/integration/tools/atlas/dbUsers.test.ts +57 -32
- package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
- package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
- package/tests/integration/tools/mongodb/read/find.test.ts +28 -0
- package/tests/unit/EJsonTransport.test.ts +71 -0
- package/tests/unit/apiClient.test.ts +193 -0
- package/tests/unit/session.test.ts +65 -0
- package/tests/unit/telemetry.test.ts +165 -71
- package/tsconfig.build.json +1 -1
- package/dist/packageInfo.js.map +0 -1
- package/dist/telemetry/device-id.js +0 -20
- package/dist/telemetry/device-id.js.map +0 -1
- package/src/telemetry/device-id.ts +0 -21
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { ApiClient } from "../../src/common/atlas/apiClient.js";
|
|
3
|
+
import { CommonProperties, TelemetryEvent, TelemetryResult } from "../../src/telemetry/types.js";
|
|
4
|
+
|
|
5
|
+
describe("ApiClient", () => {
|
|
6
|
+
let apiClient: ApiClient;
|
|
7
|
+
|
|
8
|
+
const mockEvents: TelemetryEvent<CommonProperties>[] = [
|
|
9
|
+
{
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
source: "mdbmcp",
|
|
12
|
+
properties: {
|
|
13
|
+
mcp_client_version: "1.0.0",
|
|
14
|
+
mcp_client_name: "test-client",
|
|
15
|
+
mcp_server_version: "1.0.0",
|
|
16
|
+
mcp_server_name: "test-server",
|
|
17
|
+
platform: "test-platform",
|
|
18
|
+
arch: "test-arch",
|
|
19
|
+
os_type: "test-os",
|
|
20
|
+
component: "test-component",
|
|
21
|
+
duration_ms: 100,
|
|
22
|
+
result: "success" as TelemetryResult,
|
|
23
|
+
category: "test-category",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
apiClient = new ApiClient({
|
|
30
|
+
baseUrl: "https://api.test.com",
|
|
31
|
+
credentials: {
|
|
32
|
+
clientId: "test-client-id",
|
|
33
|
+
clientSecret: "test-client-secret",
|
|
34
|
+
},
|
|
35
|
+
userAgent: "test-user-agent",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// @ts-expect-error accessing private property for testing
|
|
39
|
+
apiClient.getAccessToken = jest.fn().mockResolvedValue("mockToken");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("constructor", () => {
|
|
47
|
+
it("should create a client with the correct configuration", () => {
|
|
48
|
+
expect(apiClient).toBeDefined();
|
|
49
|
+
expect(apiClient.hasCredentials()).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("listProjects", () => {
|
|
54
|
+
it("should return a list of projects", async () => {
|
|
55
|
+
const mockProjects = {
|
|
56
|
+
results: [
|
|
57
|
+
{ id: "1", name: "Project 1" },
|
|
58
|
+
{ id: "2", name: "Project 2" },
|
|
59
|
+
],
|
|
60
|
+
totalCount: 2,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const mockGet = jest.fn().mockImplementation(() => ({
|
|
64
|
+
data: mockProjects,
|
|
65
|
+
error: null,
|
|
66
|
+
response: new Response(),
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// @ts-expect-error accessing private property for testing
|
|
70
|
+
apiClient.client.GET = mockGet;
|
|
71
|
+
|
|
72
|
+
const result = await apiClient.listProjects();
|
|
73
|
+
|
|
74
|
+
expect(mockGet).toHaveBeenCalledWith("/api/atlas/v2/groups", undefined);
|
|
75
|
+
expect(result).toEqual(mockProjects);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should throw an error when the API call fails", async () => {
|
|
79
|
+
const mockError = {
|
|
80
|
+
reason: "Test error",
|
|
81
|
+
detail: "Something went wrong",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mockGet = jest.fn().mockImplementation(() => ({
|
|
85
|
+
data: null,
|
|
86
|
+
error: mockError,
|
|
87
|
+
response: new Response(),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// @ts-expect-error accessing private property for testing
|
|
91
|
+
apiClient.client.GET = mockGet;
|
|
92
|
+
|
|
93
|
+
await expect(apiClient.listProjects()).rejects.toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("sendEvents", () => {
|
|
98
|
+
it("should send events to authenticated endpoint when token is available and valid", async () => {
|
|
99
|
+
const mockFetch = jest.spyOn(global, "fetch");
|
|
100
|
+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
101
|
+
|
|
102
|
+
await apiClient.sendEvents(mockEvents);
|
|
103
|
+
|
|
104
|
+
const url = new URL("api/private/v1.0/telemetry/events", "https://api.test.com");
|
|
105
|
+
expect(mockFetch).toHaveBeenCalledWith(url, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
Authorization: "Bearer mockToken",
|
|
110
|
+
Accept: "application/json",
|
|
111
|
+
"User-Agent": "test-user-agent",
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(mockEvents),
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should fall back to unauthenticated endpoint when token is not available via exception", async () => {
|
|
118
|
+
const mockFetch = jest.spyOn(global, "fetch");
|
|
119
|
+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
120
|
+
|
|
121
|
+
// @ts-expect-error accessing private property for testing
|
|
122
|
+
apiClient.getAccessToken = jest.fn().mockRejectedValue(new Error("No access token available"));
|
|
123
|
+
|
|
124
|
+
await apiClient.sendEvents(mockEvents);
|
|
125
|
+
|
|
126
|
+
const url = new URL("api/private/unauth/telemetry/events", "https://api.test.com");
|
|
127
|
+
expect(mockFetch).toHaveBeenCalledWith(url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
Accept: "application/json",
|
|
132
|
+
"User-Agent": "test-user-agent",
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(mockEvents),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should fall back to unauthenticated endpoint when token is undefined", async () => {
|
|
139
|
+
const mockFetch = jest.spyOn(global, "fetch");
|
|
140
|
+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
141
|
+
|
|
142
|
+
// @ts-expect-error accessing private property for testing
|
|
143
|
+
apiClient.getAccessToken = jest.fn().mockReturnValueOnce(undefined);
|
|
144
|
+
|
|
145
|
+
await apiClient.sendEvents(mockEvents);
|
|
146
|
+
|
|
147
|
+
const url = new URL("api/private/unauth/telemetry/events", "https://api.test.com");
|
|
148
|
+
expect(mockFetch).toHaveBeenCalledWith(url, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
Accept: "application/json",
|
|
153
|
+
"User-Agent": "test-user-agent",
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify(mockEvents),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should fall back to unauthenticated endpoint on 401 error", async () => {
|
|
160
|
+
const mockFetch = jest.spyOn(global, "fetch");
|
|
161
|
+
mockFetch
|
|
162
|
+
.mockResolvedValueOnce(new Response(null, { status: 401 }))
|
|
163
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
164
|
+
|
|
165
|
+
await apiClient.sendEvents(mockEvents);
|
|
166
|
+
|
|
167
|
+
const url = new URL("api/private/unauth/telemetry/events", "https://api.test.com");
|
|
168
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
169
|
+
expect(mockFetch).toHaveBeenLastCalledWith(url, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
Accept: "application/json",
|
|
174
|
+
"User-Agent": "test-user-agent",
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(mockEvents),
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should throw error when both authenticated and unauthenticated requests fail", async () => {
|
|
181
|
+
const mockFetch = jest.spyOn(global, "fetch");
|
|
182
|
+
mockFetch
|
|
183
|
+
.mockResolvedValueOnce(new Response(null, { status: 401 }))
|
|
184
|
+
.mockResolvedValueOnce(new Response(null, { status: 500 }));
|
|
185
|
+
|
|
186
|
+
const mockToken = "test-token";
|
|
187
|
+
// @ts-expect-error accessing private property for testing
|
|
188
|
+
apiClient.getAccessToken = jest.fn().mockResolvedValue(mockToken);
|
|
189
|
+
|
|
190
|
+
await expect(apiClient.sendEvents(mockEvents)).rejects.toThrow();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
|
|
3
|
+
import { Session } from "../../src/session.js";
|
|
4
|
+
import { config } from "../../src/config.js";
|
|
5
|
+
|
|
6
|
+
jest.mock("@mongosh/service-provider-node-driver");
|
|
7
|
+
const MockNodeDriverServiceProvider = NodeDriverServiceProvider as jest.MockedClass<typeof NodeDriverServiceProvider>;
|
|
8
|
+
|
|
9
|
+
describe("Session", () => {
|
|
10
|
+
let session: Session;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
session = new Session({
|
|
13
|
+
apiClientId: "test-client-id",
|
|
14
|
+
apiBaseUrl: "https://api.test.com",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
MockNodeDriverServiceProvider.connect = jest.fn(() =>
|
|
18
|
+
Promise.resolve({} as unknown as NodeDriverServiceProvider)
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("connectToMongoDB", () => {
|
|
23
|
+
const testCases: {
|
|
24
|
+
connectionString: string;
|
|
25
|
+
expectAppName: boolean;
|
|
26
|
+
name: string;
|
|
27
|
+
}[] = [
|
|
28
|
+
{
|
|
29
|
+
connectionString: "mongodb://localhost:27017",
|
|
30
|
+
expectAppName: true,
|
|
31
|
+
name: "db without appName",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
connectionString: "mongodb://localhost:27017?appName=CustomAppName",
|
|
35
|
+
expectAppName: false,
|
|
36
|
+
name: "db with custom appName",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
connectionString:
|
|
40
|
+
"mongodb+srv://test.mongodb.net/test?retryWrites=true&w=majority&appName=CustomAppName",
|
|
41
|
+
expectAppName: false,
|
|
42
|
+
name: "atlas db with custom appName",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const testCase of testCases) {
|
|
47
|
+
it(`should update connection string for ${testCase.name}`, async () => {
|
|
48
|
+
await session.connectToMongoDB(testCase.connectionString, config.connectOptions);
|
|
49
|
+
expect(session.serviceProvider).toBeDefined();
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
52
|
+
const connectMock = MockNodeDriverServiceProvider.connect as jest.Mock<
|
|
53
|
+
typeof NodeDriverServiceProvider.connect
|
|
54
|
+
>;
|
|
55
|
+
expect(connectMock).toHaveBeenCalledOnce();
|
|
56
|
+
const connectionString = connectMock.mock.calls[0][0];
|
|
57
|
+
if (testCase.expectAppName) {
|
|
58
|
+
expect(connectionString).toContain("appName=MongoDB+MCP+Server");
|
|
59
|
+
} else {
|
|
60
|
+
expect(connectionString).not.toContain("appName=MongoDB+MCP+Server");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ApiClient } from "../../src/common/atlas/apiClient.js";
|
|
2
2
|
import { Session } from "../../src/session.js";
|
|
3
|
-
import { Telemetry } from "../../src/telemetry/telemetry.js";
|
|
3
|
+
import { DEVICE_ID_TIMEOUT, Telemetry } from "../../src/telemetry/telemetry.js";
|
|
4
4
|
import { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js";
|
|
5
5
|
import { EventCache } from "../../src/telemetry/eventCache.js";
|
|
6
6
|
import { config } from "../../src/config.js";
|
|
7
7
|
import { jest } from "@jest/globals";
|
|
8
|
+
import logger, { LogId } from "../../src/logger.js";
|
|
9
|
+
import { createHmac } from "crypto";
|
|
8
10
|
|
|
9
11
|
// Mock the ApiClient to avoid real API calls
|
|
10
12
|
jest.mock("../../src/common/atlas/apiClient.js");
|
|
@@ -15,6 +17,9 @@ jest.mock("../../src/telemetry/eventCache.js");
|
|
|
15
17
|
const MockEventCache = EventCache as jest.MockedClass<typeof EventCache>;
|
|
16
18
|
|
|
17
19
|
describe("Telemetry", () => {
|
|
20
|
+
const machineId = "test-machine-id";
|
|
21
|
+
const hashedMachineId = createHmac("sha256", machineId.toUpperCase()).update("atlascli").digest("hex");
|
|
22
|
+
|
|
18
23
|
let mockApiClient: jest.Mocked<ApiClient>;
|
|
19
24
|
let mockEventCache: jest.Mocked<EventCache>;
|
|
20
25
|
let session: Session;
|
|
@@ -120,109 +125,198 @@ describe("Telemetry", () => {
|
|
|
120
125
|
setAgentRunner: jest.fn().mockResolvedValue(undefined),
|
|
121
126
|
} as unknown as Session;
|
|
122
127
|
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
telemetry = Telemetry.create(session, config, {
|
|
129
|
+
eventCache: mockEventCache,
|
|
130
|
+
getRawMachineId: () => Promise.resolve(machineId),
|
|
131
|
+
});
|
|
132
|
+
|
|
125
133
|
config.telemetry = "enabled";
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
describe("
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
describe("sending events", () => {
|
|
137
|
+
describe("when telemetry is enabled", () => {
|
|
138
|
+
it("should send events successfully", async () => {
|
|
139
|
+
const testEvent = createTestEvent();
|
|
131
140
|
|
|
132
|
-
|
|
141
|
+
await telemetry.emitEvents([testEvent]);
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
verifyMockCalls({
|
|
144
|
+
sendEventsCalls: 1,
|
|
145
|
+
clearEventsCalls: 1,
|
|
146
|
+
sendEventsCalledWith: [testEvent],
|
|
147
|
+
});
|
|
138
148
|
});
|
|
139
|
-
});
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
it("should cache events when sending fails", async () => {
|
|
151
|
+
mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error"));
|
|
143
152
|
|
|
144
|
-
|
|
153
|
+
const testEvent = createTestEvent();
|
|
145
154
|
|
|
146
|
-
|
|
155
|
+
await telemetry.emitEvents([testEvent]);
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
verifyMockCalls({
|
|
158
|
+
sendEventsCalls: 1,
|
|
159
|
+
appendEventsCalls: 1,
|
|
160
|
+
appendEventsCalledWith: [testEvent],
|
|
161
|
+
});
|
|
152
162
|
});
|
|
153
|
-
});
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
it("should include cached events when sending", async () => {
|
|
165
|
+
const cachedEvent = createTestEvent({
|
|
166
|
+
command: "cached-command",
|
|
167
|
+
component: "cached-component",
|
|
168
|
+
});
|
|
160
169
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
const newEvent = createTestEvent({
|
|
171
|
+
command: "new-command",
|
|
172
|
+
component: "new-component",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Set up mock to return cached events
|
|
176
|
+
mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]);
|
|
177
|
+
|
|
178
|
+
await telemetry.emitEvents([newEvent]);
|
|
179
|
+
|
|
180
|
+
verifyMockCalls({
|
|
181
|
+
sendEventsCalls: 1,
|
|
182
|
+
clearEventsCalls: 1,
|
|
183
|
+
sendEventsCalledWith: [cachedEvent, newEvent],
|
|
184
|
+
});
|
|
164
185
|
});
|
|
165
186
|
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
it("should correctly add common properties to events", () => {
|
|
188
|
+
const commonProps = telemetry.getCommonProperties();
|
|
168
189
|
|
|
169
|
-
|
|
190
|
+
// Use explicit type assertion
|
|
191
|
+
const expectedProps: Record<string, string> = {
|
|
192
|
+
mcp_client_version: "1.0.0",
|
|
193
|
+
mcp_client_name: "test-agent",
|
|
194
|
+
session_id: "test-session-id",
|
|
195
|
+
config_atlas_auth: "true",
|
|
196
|
+
config_connection_string: expect.any(String) as unknown as string,
|
|
197
|
+
device_id: hashedMachineId,
|
|
198
|
+
};
|
|
170
199
|
|
|
171
|
-
|
|
172
|
-
sendEventsCalls: 1,
|
|
173
|
-
clearEventsCalls: 1,
|
|
174
|
-
sendEventsCalledWith: [cachedEvent, newEvent],
|
|
200
|
+
expect(commonProps).toMatchObject(expectedProps);
|
|
175
201
|
});
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
202
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
describe("machine ID resolution", () => {
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
jest.clearAllMocks();
|
|
206
|
+
jest.useFakeTimers();
|
|
207
|
+
});
|
|
183
208
|
|
|
184
|
-
|
|
185
|
-
|
|
209
|
+
afterEach(() => {
|
|
210
|
+
jest.clearAllMocks();
|
|
211
|
+
jest.useRealTimers();
|
|
212
|
+
});
|
|
186
213
|
|
|
187
|
-
|
|
214
|
+
it("should successfully resolve the machine ID", async () => {
|
|
215
|
+
telemetry = Telemetry.create(session, config, {
|
|
216
|
+
getRawMachineId: () => Promise.resolve(machineId),
|
|
217
|
+
});
|
|
188
218
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
});
|
|
219
|
+
expect(telemetry["isBufferingEvents"]).toBe(true);
|
|
220
|
+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
|
|
192
221
|
|
|
193
|
-
|
|
194
|
-
const commonProps = telemetry.getCommonProperties();
|
|
222
|
+
await telemetry.deviceIdPromise;
|
|
195
223
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
mcp_client_name: "test-agent",
|
|
200
|
-
session_id: "test-session-id",
|
|
201
|
-
config_atlas_auth: "true",
|
|
202
|
-
config_connection_string: expect.any(String) as unknown as string,
|
|
203
|
-
};
|
|
224
|
+
expect(telemetry["isBufferingEvents"]).toBe(false);
|
|
225
|
+
expect(telemetry.getCommonProperties().device_id).toBe(hashedMachineId);
|
|
226
|
+
});
|
|
204
227
|
|
|
205
|
-
|
|
206
|
-
|
|
228
|
+
it("should handle machine ID resolution failure", async () => {
|
|
229
|
+
const loggerSpy = jest.spyOn(logger, "debug");
|
|
230
|
+
|
|
231
|
+
telemetry = Telemetry.create(session, config, {
|
|
232
|
+
getRawMachineId: () => Promise.reject(new Error("Failed to get device ID")),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(telemetry["isBufferingEvents"]).toBe(true);
|
|
236
|
+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
|
|
237
|
+
|
|
238
|
+
await telemetry.deviceIdPromise;
|
|
239
|
+
|
|
240
|
+
expect(telemetry["isBufferingEvents"]).toBe(false);
|
|
241
|
+
expect(telemetry.getCommonProperties().device_id).toBe("unknown");
|
|
242
|
+
|
|
243
|
+
expect(loggerSpy).toHaveBeenCalledWith(
|
|
244
|
+
LogId.telemetryDeviceIdFailure,
|
|
245
|
+
"telemetry",
|
|
246
|
+
"Error: Failed to get device ID"
|
|
247
|
+
);
|
|
248
|
+
});
|
|
207
249
|
|
|
208
|
-
|
|
209
|
-
|
|
250
|
+
it("should timeout if machine ID resolution takes too long", async () => {
|
|
251
|
+
const loggerSpy = jest.spyOn(logger, "debug");
|
|
210
252
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
253
|
+
telemetry = Telemetry.create(session, config, { getRawMachineId: () => new Promise(() => {}) });
|
|
254
|
+
|
|
255
|
+
expect(telemetry["isBufferingEvents"]).toBe(true);
|
|
256
|
+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
|
|
257
|
+
|
|
258
|
+
jest.advanceTimersByTime(DEVICE_ID_TIMEOUT / 2);
|
|
259
|
+
|
|
260
|
+
// Make sure the timeout doesn't happen prematurely.
|
|
261
|
+
expect(telemetry["isBufferingEvents"]).toBe(true);
|
|
262
|
+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
|
|
263
|
+
|
|
264
|
+
jest.advanceTimersByTime(DEVICE_ID_TIMEOUT);
|
|
265
|
+
|
|
266
|
+
await telemetry.deviceIdPromise;
|
|
267
|
+
|
|
268
|
+
expect(telemetry.getCommonProperties().device_id).toBe("unknown");
|
|
269
|
+
expect(telemetry["isBufferingEvents"]).toBe(false);
|
|
270
|
+
expect(loggerSpy).toHaveBeenCalledWith(
|
|
271
|
+
LogId.telemetryDeviceIdTimeout,
|
|
272
|
+
"telemetry",
|
|
273
|
+
"Device ID retrieval timed out"
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
214
277
|
});
|
|
215
278
|
|
|
216
|
-
|
|
217
|
-
|
|
279
|
+
describe("when telemetry is disabled", () => {
|
|
280
|
+
beforeEach(() => {
|
|
281
|
+
config.telemetry = "disabled";
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
afterEach(() => {
|
|
285
|
+
config.telemetry = "enabled";
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should not send events", async () => {
|
|
289
|
+
const testEvent = createTestEvent();
|
|
290
|
+
|
|
291
|
+
await telemetry.emitEvents([testEvent]);
|
|
292
|
+
|
|
293
|
+
verifyMockCalls();
|
|
294
|
+
});
|
|
218
295
|
});
|
|
219
296
|
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
describe("when DO_NOT_TRACK environment variable is set", () => {
|
|
298
|
+
let originalEnv: string | undefined;
|
|
299
|
+
|
|
300
|
+
beforeEach(() => {
|
|
301
|
+
originalEnv = process.env.DO_NOT_TRACK;
|
|
302
|
+
process.env.DO_NOT_TRACK = "1";
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
afterEach(() => {
|
|
306
|
+
if (originalEnv) {
|
|
307
|
+
process.env.DO_NOT_TRACK = originalEnv;
|
|
308
|
+
} else {
|
|
309
|
+
delete process.env.DO_NOT_TRACK;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
222
312
|
|
|
223
|
-
|
|
313
|
+
it("should not send events", async () => {
|
|
314
|
+
const testEvent = createTestEvent();
|
|
224
315
|
|
|
225
|
-
|
|
316
|
+
await telemetry.emitEvents([testEvent]);
|
|
317
|
+
|
|
318
|
+
verifyMockCalls();
|
|
319
|
+
});
|
|
226
320
|
});
|
|
227
321
|
});
|
|
228
322
|
});
|
package/tsconfig.build.json
CHANGED
package/dist/packageInfo.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"packageInfo.js","sourceRoot":"","sources":["../src/packageInfo.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEhE,MAAM,CAAC,MAAM,WAAW,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC,OAAO;IAC5B,aAAa,EAAE,oBAAoB;CACtC,CAAC"}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { createHmac } from "crypto";
|
|
2
|
-
import nodeMachineId from "node-machine-id";
|
|
3
|
-
import logger, { LogId } from "../logger.js";
|
|
4
|
-
export function getDeviceId() {
|
|
5
|
-
try {
|
|
6
|
-
const originalId = nodeMachineId.machineIdSync(true);
|
|
7
|
-
// Create a hashed format from the all uppercase version of the machine ID
|
|
8
|
-
// to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses.
|
|
9
|
-
const hmac = createHmac("sha256", originalId.toUpperCase());
|
|
10
|
-
/** This matches the message used to create the hashes in Atlas CLI */
|
|
11
|
-
const DEVICE_ID_HASH_MESSAGE = "atlascli";
|
|
12
|
-
hmac.update(DEVICE_ID_HASH_MESSAGE);
|
|
13
|
-
return hmac.digest("hex");
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
|
|
17
|
-
return "unknown";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
//# sourceMappingURL=device-id.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"device-id.js","sourceRoot":"","sources":["../../src/telemetry/device-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAC5C,OAAO,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,UAAU,WAAW;IACvB,IAAI,CAAC;QACD,MAAM,UAAU,GAAG,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACrD,0EAA0E;QAC1E,oFAAoF;QACpF,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;QAE5D,sEAAsE;QACtE,MAAM,sBAAsB,GAAG,UAAU,CAAC;QAE1C,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,wBAAwB,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACzE,OAAO,SAAS,CAAC;IACrB,CAAC;AACL,CAAC"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { createHmac } from "crypto";
|
|
2
|
-
import nodeMachineId from "node-machine-id";
|
|
3
|
-
import logger, { LogId } from "../logger.js";
|
|
4
|
-
|
|
5
|
-
export function getDeviceId(): string {
|
|
6
|
-
try {
|
|
7
|
-
const originalId = nodeMachineId.machineIdSync(true);
|
|
8
|
-
// Create a hashed format from the all uppercase version of the machine ID
|
|
9
|
-
// to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses.
|
|
10
|
-
const hmac = createHmac("sha256", originalId.toUpperCase());
|
|
11
|
-
|
|
12
|
-
/** This matches the message used to create the hashes in Atlas CLI */
|
|
13
|
-
const DEVICE_ID_HASH_MESSAGE = "atlascli";
|
|
14
|
-
|
|
15
|
-
hmac.update(DEVICE_ID_HASH_MESSAGE);
|
|
16
|
-
return hmac.digest("hex");
|
|
17
|
-
} catch (error) {
|
|
18
|
-
logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
|
|
19
|
-
return "unknown";
|
|
20
|
-
}
|
|
21
|
-
}
|