Skip to content

Commit d72c835

Browse files
authored
feat: add logging (#35)
1 parent e303818 commit d72c835

File tree

8 files changed

+166
-12
lines changed

8 files changed

+166
-12
lines changed

package-lock.json

Lines changed: 8 additions & 2 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
@@ -55,6 +55,8 @@
5555
"@types/express": "^5.0.1",
5656
"bson": "^6.10.3",
5757
"mongodb": "^6.15.0",
58+
"mongodb-log-writer": "^2.4.1",
59+
"mongodb-redact": "^1.1.6",
5860
"mongodb-schema": "^12.6.2",
5961
"zod": "^3.24.2"
6062
},

src/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from "path";
22
import fs from "fs";
33
import { fileURLToPath } from "url";
4+
import os from "os";
45

56
const __filename = fileURLToPath(import.meta.url);
67
const __dirname = path.dirname(__filename);
@@ -16,6 +17,19 @@ export const config = {
1617
stateFile: process.env.STATE_FILE || path.resolve("./state.json"),
1718
projectID: process.env.PROJECT_ID,
1819
userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
20+
localDataPath: getLocalDataPath(),
1921
};
2022

2123
export default config;
24+
25+
function getLocalDataPath() {
26+
if (process.platform === "win32") {
27+
const appData = process.env.APPDATA;
28+
const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA;
29+
if (localAppData && appData) {
30+
return path.join(localAppData, "mongodb", "mongodb-mcp");
31+
}
32+
}
33+
34+
return path.join(os.homedir(), ".mongodb", "mongodb-mcp");
35+
}

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
import { Server } from "./server.js";
3+
import logger from "./logger.js";
4+
import { mongoLogId } from "mongodb-log-writer";
35

46
async function runServer() {
57
const server = new Server();
@@ -9,6 +11,7 @@ async function runServer() {
911
}
1012

1113
runServer().catch((error) => {
12-
console.error(`Fatal error running server:`, error);
14+
logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`);
15+
1316
process.exit(1);
1417
});

src/logger.ts

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,119 @@
1-
// TODO: use a proper logger here
2-
export function log(level: string, message: string) {
3-
console.error(`[${level.toUpperCase()}] ${message}`);
1+
import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer";
2+
import path from "path";
3+
import config from "./config.js";
4+
import redact from "mongodb-redact";
5+
import fs from "fs/promises";
6+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js";
8+
9+
export type LogLevel = LoggingMessageNotification["params"]["level"];
10+
11+
abstract class LoggerBase {
12+
abstract log(level: LogLevel, id: MongoLogId, context: string, message: string): void;
13+
info(id: MongoLogId, context: string, message: string): void {
14+
this.log("info", id, context, message);
15+
}
16+
17+
error(id: MongoLogId, context: string, message: string): void {
18+
this.log("error", id, context, message);
19+
}
20+
debug(id: MongoLogId, context: string, message: string): void {
21+
this.log("debug", id, context, message);
22+
}
23+
24+
notice(id: MongoLogId, context: string, message: string): void {
25+
this.log("notice", id, context, message);
26+
}
27+
28+
warning(id: MongoLogId, context: string, message: string): void {
29+
this.log("warning", id, context, message);
30+
}
31+
32+
critical(id: MongoLogId, context: string, message: string): void {
33+
this.log("critical", id, context, message);
34+
}
35+
36+
alert(id: MongoLogId, context: string, message: string): void {
37+
this.log("alert", id, context, message);
38+
}
39+
40+
emergency(id: MongoLogId, context: string, message: string): void {
41+
this.log("emergency", id, context, message);
42+
}
43+
}
44+
45+
class ConsoleLogger extends LoggerBase {
46+
log(level: LogLevel, id: MongoLogId, context: string, message: string): void {
47+
message = redact(message);
48+
console.error(`[${level.toUpperCase()}] ${id} - ${context}: ${message}`);
49+
}
50+
}
51+
52+
class Logger extends LoggerBase {
53+
constructor(
54+
private logWriter: MongoLogWriter,
55+
private server: McpServer
56+
) {
57+
super();
58+
}
59+
60+
log(level: LogLevel, id: MongoLogId, context: string, message: string): void {
61+
message = redact(message);
62+
const mongoDBLevel = this.mapToMongoDBLogLevel(level);
63+
this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message);
64+
this.server.server.sendLoggingMessage({
65+
level,
66+
data: `[${context}]: ${message}`,
67+
});
68+
}
69+
70+
private mapToMongoDBLogLevel(level: LogLevel): "info" | "warn" | "error" | "debug" | "fatal" {
71+
switch (level) {
72+
case "info":
73+
return "info";
74+
case "warning":
75+
return "warn";
76+
case "error":
77+
return "error";
78+
case "notice":
79+
case "debug":
80+
return "debug";
81+
case "critical":
82+
case "alert":
83+
case "emergency":
84+
return "fatal";
85+
default:
86+
return "info";
87+
}
88+
}
89+
}
90+
91+
class ProxyingLogger extends LoggerBase {
92+
private internalLogger: LoggerBase = new ConsoleLogger();
93+
94+
log(level: LogLevel, id: MongoLogId, context: string, message: string): void {
95+
this.internalLogger.log(level, id, context, message);
96+
}
97+
}
98+
99+
const logger = new ProxyingLogger();
100+
export default logger;
101+
102+
export async function initializeLogger(server: McpServer): Promise<void> {
103+
const logDir = path.join(config.localDataPath, ".app-logs");
104+
await fs.mkdir(logDir, { recursive: true });
105+
106+
const manager = new MongoLogManager({
107+
directory: path.join(config.localDataPath, ".app-logs"),
108+
retentionDays: 30,
109+
onwarn: console.warn,
110+
onerror: console.error,
111+
gzip: false,
112+
retentionGB: 1,
113+
});
114+
115+
await manager.cleanupOldLogFiles();
116+
117+
const logWriter = await manager.createLogWriter();
118+
logger["internalLogger"] = new Logger(logWriter, server);
4119
}

src/server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
55
import { registerAtlasTools } from "./tools/atlas/tools.js";
66
import { registerMongoDBTools } from "./tools/mongodb/index.js";
77
import { config } from "./config.js";
8+
import logger, { initializeLogger } from "./logger.js";
9+
import { mongoLogId } from "mongodb-log-writer";
810

911
export class Server {
1012
state: State | undefined = undefined;
@@ -39,6 +41,8 @@ export class Server {
3941
version: config.version,
4042
});
4143

44+
server.server.registerCapabilities({ logging: {} });
45+
4246
registerAtlasTools(server, this.state!, this.apiClient!);
4347
registerMongoDBTools(server, this.state!);
4448

@@ -49,5 +53,8 @@ export class Server {
4953
await this.init();
5054
const server = this.createMcpServer();
5155
await server.connect(transport);
56+
await initializeLogger(server);
57+
58+
logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`);
5259
}
5360
}

src/tools/atlas/auth.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2-
import { log } from "../../logger.js";
32
import { saveState } from "../../state.js";
43
import { AtlasToolBase } from "./atlasTool.js";
54
import { isAuthenticated } from "../../common/atlas/auth.js";
5+
import logger from "../../logger.js";
6+
import { mongoLogId } from "mongodb-log-writer";
67

78
export class AuthTool extends AtlasToolBase {
89
protected name = "atlas-auth";
@@ -15,7 +16,7 @@ export class AuthTool extends AtlasToolBase {
1516

1617
async execute(): Promise<CallToolResult> {
1718
if (await this.isAuthenticated()) {
18-
log("INFO", "Already authenticated!");
19+
logger.debug(mongoLogId(1_000_001), "auth", "Already authenticated!");
1920
return {
2021
content: [{ type: "text", text: "You are already authenticated!" }],
2122
};
@@ -40,13 +41,13 @@ export class AuthTool extends AtlasToolBase {
4041
};
4142
} catch (error: unknown) {
4243
if (error instanceof Error) {
43-
log("error", `Authentication error: ${error}`);
44+
logger.error(mongoLogId(1_000_002), "auth", `Authentication error: ${error}`);
4445
return {
4546
content: [{ type: "text", text: `Authentication failed: ${error.message}` }],
4647
};
4748
}
4849

49-
log("error", `Unknown authentication error: ${error}`);
50+
logger.error(mongoLogId(1_000_003), "auth", `Unknown authentication error: ${error}`);
5051
return {
5152
content: [{ type: "text", text: "Authentication failed due to an unknown error." }],
5253
};

src/tools/tool.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { z, ZodNever, ZodRawShape } from "zod";
3-
import { log } from "../logger.js";
43
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
54
import { State } from "../state.js";
5+
import logger from "../logger.js";
6+
import { mongoLogId } from "mongodb-log-writer";
67

78
export type ToolArgs<Args extends ZodRawShape> = z.objectOutputType<Args, ZodNever>;
89

@@ -21,10 +22,15 @@ export abstract class ToolBase {
2122
const callback = async (args: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> => {
2223
try {
2324
// TODO: add telemetry here
25+
logger.debug(
26+
mongoLogId(1_000_006),
27+
"tool",
28+
`Executing ${this.name} with args: ${JSON.stringify(args)}`
29+
);
2430

2531
return await this.execute(args);
2632
} catch (error) {
27-
log("error", `Error executing ${this.name}: ${error}`);
33+
logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error}`);
2834

2935
// If the error is authentication related, suggest using auth tool
3036
if (error instanceof Error && error.message.includes("Not authenticated")) {

0 commit comments

Comments
 (0)