Skip to content

Commit 8adc20d

Browse files
committed
add cache and client logic
1 parent 40aaef0 commit 8adc20d

File tree

4 files changed

+155
-52
lines changed

4 files changed

+155
-52
lines changed

src/common/atlas/apiClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { AccessToken, ClientCredentials } from "simple-oauth2";
55
import { ApiClientError } from "./apiClientError.js";
66
import { paths, operations } from "./openapi.js";
77
import { BaseEvent } from "../../telemetry/types.js";
8+
import { mongoLogId } from "mongodb-log-writer";
9+
import logger from "../../logger.js";
810

911
const ATLAS_API_VERSION = "2025-03-12";
1012

@@ -91,6 +93,12 @@ export class ApiClient {
9193
this.client.use(this.authMiddleware);
9294
}
9395
this.client.use(this.errorMiddleware);
96+
logger.info(mongoLogId(1_000_000), "api-client", `Initialized API client with credentials: ${this.hasCredentials()}`);
97+
}
98+
99+
public hasCredentials(): boolean {
100+
logger.info(mongoLogId(1_000_000), "api-client", `Checking if API client has credentials: ${!!(this.oauth2Client && this.accessToken)}`);
101+
return !!(this.oauth2Client && this.accessToken);
94102
}
95103

96104
public async getIpInfo(): Promise<{

src/session.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import { Implementation } from "@modelcontextprotocol/sdk/types.js";
88
export class Session {
99
sessionId?: string;
1010
serviceProvider?: NodeDriverServiceProvider;
11-
apiClient?: ApiClient;
11+
apiClient: ApiClient;
1212
agentClientName?: string;
1313
agentClientVersion?: string;
1414

1515
constructor() {
16-
// configure api client if credentials are set
16+
// Initialize API client with credentials if available
1717
if (config.apiClientId && config.apiClientSecret) {
1818
this.apiClient = new ApiClient({
1919
baseUrl: config.apiBaseUrl,
@@ -22,7 +22,11 @@ export class Session {
2222
clientSecret: config.apiClientSecret,
2323
},
2424
});
25+
return;
2526
}
27+
28+
// Initialize API client without credentials
29+
this.apiClient = new ApiClient({ baseUrl: config.apiBaseUrl });
2630
}
2731

2832
setAgentClientData(agentClient: Implementation | undefined) {
@@ -31,13 +35,14 @@ export class Session {
3135
}
3236

3337
ensureAuthenticated(): asserts this is { apiClient: ApiClient } {
34-
if (!this.apiClient) {
38+
if (!this.apiClient || !(this.apiClient.hasCredentials())) {
3539
if (!config.apiClientId || !config.apiClientSecret) {
3640
throw new Error(
3741
"Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables."
3842
);
3943
}
4044

45+
// Initialize or reinitialize API client with credentials
4146
this.apiClient = new ApiClient({
4247
baseUrl: config.apiBaseUrl,
4348
credentials: {

src/telemetry/telemetry.ts

Lines changed: 136 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,119 +5,206 @@ import config from '../config.js';
55
import logger from '../logger.js';
66
import { mongoLogId } from 'mongodb-log-writer';
77
import { ApiClient } from '../common/atlas/apiClient.js';
8-
import { ApiClientError } from '../common/atlas/apiClientError.js';
98
import fs from 'fs/promises';
109
import path from 'path';
1110

12-
const isTelemetryEnabled = config.telemetry === 'enabled';
11+
const TELEMETRY_ENABLED = config.telemetry !== 'disabled';
1312
const CACHE_FILE = path.join(process.cwd(), '.telemetry-cache.json');
1413

14+
interface TelemetryError extends Error {
15+
code?: string;
16+
}
17+
18+
type EventResult = {
19+
success: boolean;
20+
error?: Error;
21+
};
22+
23+
type CommonProperties = {
24+
device_id: string;
25+
mcp_server_version: string;
26+
mcp_server_name: string;
27+
mcp_client_version?: string;
28+
mcp_client_name?: string;
29+
platform: string;
30+
arch: string;
31+
os_type: string;
32+
os_version?: string;
33+
session_id?: string;
34+
};
35+
1536
export class Telemetry {
16-
constructor(private readonly session: Session) {}
37+
private readonly commonProperties: CommonProperties;
1738

18-
private readonly commonProperties = {
39+
constructor(private readonly session: Session) {
40+
// Ensure all required properties are present
41+
this.commonProperties = Object.freeze({
42+
device_id: config.device_id,
1943
mcp_server_version: pkg.version,
2044
mcp_server_name: config.mcpServerName,
2145
mcp_client_version: this.session.agentClientVersion,
2246
mcp_client_name: this.session.agentClientName,
23-
session_id: this.session.sessionId,
24-
device_id: config.device_id,
2547
platform: config.platform,
2648
arch: config.arch,
2749
os_type: config.os_type,
2850
os_version: config.os_version,
29-
};
51+
});
52+
}
3053

31-
async emitToolEvent(command: string, category: string, startTime: number, result: 'success' | 'failure', error?: Error): Promise<void> {
32-
if (!isTelemetryEnabled) {
33-
logger.debug(mongoLogId(1_000_000), "telemetry", `Telemetry is disabled, skipping event.`);
54+
/**
55+
* Emits a tool event with timing and error information
56+
* @param command - The command being executed
57+
* @param category - Category of the command
58+
* @param startTime - Start time in milliseconds
59+
* @param result - Whether the command succeeded or failed
60+
* @param error - Optional error if the command failed
61+
*/
62+
public async emitToolEvent(
63+
command: string,
64+
category: string,
65+
startTime: number,
66+
result: 'success' | 'failure',
67+
error?: Error
68+
): Promise<void> {
69+
if (!TELEMETRY_ENABLED) {
70+
logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping event.");
3471
return;
3572
}
3673

74+
const event = this.createToolEvent(command, category, startTime, result, error);
75+
await this.emit([event]);
76+
}
77+
78+
/**
79+
* Creates a tool event with common properties and timing information
80+
*/
81+
private createToolEvent(
82+
command: string,
83+
category: string,
84+
startTime: number,
85+
result: 'success' | 'failure',
86+
error?: Error
87+
): ToolEvent {
3788
const duration = Date.now() - startTime;
3889

3990
const event: ToolEvent = {
4091
timestamp: new Date().toISOString(),
4192
source: 'mdbmcp',
4293
properties: {
4394
...this.commonProperties,
44-
command: command,
45-
category: category,
95+
command,
96+
category,
4697
duration_ms: duration,
47-
result: result
98+
session_id: this.session.sessionId,
99+
result,
100+
...(error && {
101+
error_type: error.name,
102+
error_code: error.message
103+
})
48104
}
49105
};
50106

51-
if (result === 'failure') {
52-
event.properties.error_type = error?.name;
53-
event.properties.error_code = error?.message;
54-
}
55-
56-
await this.emit([event]);
107+
return event;
57108
}
58109

110+
/**
111+
* Attempts to emit events through authenticated and unauthenticated clients
112+
* Falls back to caching if both attempts fail
113+
*/
59114
private async emit(events: BaseEvent[]): Promise<void> {
60-
// First try to read any cached events
61115
const cachedEvents = await this.readCache();
62116
const allEvents = [...cachedEvents, ...events];
63117

64-
logger.debug(mongoLogId(1_000_000), "telemetry", `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`);
118+
logger.debug(
119+
mongoLogId(1_000_000),
120+
"telemetry",
121+
`Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
122+
);
65123

66-
try {
67-
if (this.session.apiClient) {
68-
await this.session.apiClient.sendEvents(allEvents);
69-
// If successful, clear the cache
70-
await this.clearCache();
71-
return;
72-
}
73-
} catch (error) {
74-
logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to authenticated client: ${error}`);
75-
// Cache the events that failed to send
76-
await this.cacheEvents(allEvents);
124+
const result = await this.sendEvents(this.session.apiClient, allEvents);
125+
if (result.success) {
126+
await this.clearCache();
127+
return;
77128
}
78129

79-
// Try unauthenticated client as fallback
130+
logger.warning(
131+
mongoLogId(1_000_000),
132+
"telemetry",
133+
`Error sending event to client: ${result.error}`
134+
);
135+
await this.cacheEvents(allEvents);
136+
}
137+
138+
/**
139+
* Attempts to send events through the provided API client
140+
*/
141+
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
80142
try {
81-
const tempApiClient = new ApiClient({
82-
baseUrl: config.apiBaseUrl,
83-
});
84-
await tempApiClient.sendEvents(allEvents);
85-
// If successful, clear the cache
86-
await this.clearCache();
143+
await client.sendEvents(events);
144+
return { success: true };
87145
} catch (error) {
88-
logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to unauthenticated client: ${error}`);
89-
// Cache the events that failed to send
90-
await this.cacheEvents(allEvents);
146+
return {
147+
success: false,
148+
error: error instanceof Error ? error : new Error(String(error))
149+
};
91150
}
92151
}
93152

153+
/**
154+
* Reads cached events from disk
155+
* Returns empty array if no cache exists or on read error
156+
*/
94157
private async readCache(): Promise<BaseEvent[]> {
95158
try {
96159
const data = await fs.readFile(CACHE_FILE, 'utf-8');
97-
return JSON.parse(data);
160+
return JSON.parse(data) as BaseEvent[];
98161
} catch (error) {
99-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
100-
logger.warning(mongoLogId(1_000_000), "telemetry", `Error reading telemetry cache: ${error}`);
162+
const typedError = error as TelemetryError;
163+
if (typedError.code !== 'ENOENT') {
164+
logger.warning(
165+
mongoLogId(1_000_000),
166+
"telemetry",
167+
`Error reading telemetry cache: ${typedError.message}`
168+
);
101169
}
102170
return [];
103171
}
104172
}
105173

174+
/**
175+
* Caches events to disk for later sending
176+
*/
106177
private async cacheEvents(events: BaseEvent[]): Promise<void> {
107178
try {
108179
await fs.writeFile(CACHE_FILE, JSON.stringify(events, null, 2));
109-
logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events for later sending`);
180+
logger.debug(
181+
mongoLogId(1_000_000),
182+
"telemetry",
183+
`Cached ${events.length} events for later sending`
184+
);
110185
} catch (error) {
111-
logger.warning(mongoLogId(1_000_000), "telemetry", `Failed to cache telemetry events: ${error}`);
186+
logger.warning(
187+
mongoLogId(1_000_000),
188+
"telemetry",
189+
`Failed to cache telemetry events: ${error instanceof Error ? error.message : String(error)}`
190+
);
112191
}
113192
}
114193

194+
/**
195+
* Clears the event cache after successful sending
196+
*/
115197
private async clearCache(): Promise<void> {
116198
try {
117199
await fs.unlink(CACHE_FILE);
118200
} catch (error) {
119-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
120-
logger.warning(mongoLogId(1_000_000), "telemetry", `Error clearing telemetry cache: ${error}`);
201+
const typedError = error as TelemetryError;
202+
if (typedError.code !== 'ENOENT') {
203+
logger.warning(
204+
mongoLogId(1_000_000),
205+
"telemetry",
206+
`Error clearing telemetry cache: ${typedError.message}`
207+
);
121208
}
122209
}
123210
}

src/tools/atlas/listProjects.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
22
import { AtlasToolBase } from "./atlasTool.js";
33
import { OperationType } from "../tool.js";
4+
import { mongoLogId } from "mongodb-log-writer";
5+
import logger from "../../logger.js";
46

57
export class ListProjectsTool extends AtlasToolBase {
68
protected name = "atlas-list-projects";
@@ -10,6 +12,7 @@ export class ListProjectsTool extends AtlasToolBase {
1012

1113
protected async execute(): Promise<CallToolResult> {
1214
this.session.ensureAuthenticated();
15+
logger.info(mongoLogId(1_000_000), "session", `Called ensureAuthenticated! `);
1316

1417
const data = await this.session.apiClient.listProjects();
1518

0 commit comments

Comments
 (0)