From 18671f14dcfddb6fc9d0814cc4bf30e190318f41 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 24 Apr 2025 14:35:02 +0200 Subject: [PATCH 1/2] feat: add logs tool --- src/tools/mongodb/metadata/logs.ts | 56 +++++++++++++ src/tools/mongodb/tools.ts | 2 + tests/integration/helpers.ts | 8 +- .../tools/mongodb/delete/dropDatabase.test.ts | 3 +- .../tools/mongodb/metadata/dbStats.test.ts | 18 ++-- .../tools/mongodb/metadata/logs.test.ts | 83 +++++++++++++++++++ 6 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 src/tools/mongodb/metadata/logs.ts create mode 100644 tests/integration/tools/mongodb/metadata/logs.test.ts diff --git a/src/tools/mongodb/metadata/logs.ts b/src/tools/mongodb/metadata/logs.ts new file mode 100644 index 00000000..434c23ca --- /dev/null +++ b/src/tools/mongodb/metadata/logs.ts @@ -0,0 +1,56 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { MongoDBToolBase } from "../mongodbTool.js"; +import { ToolArgs, OperationType } from "../../tool.js"; +import { EJSON } from "bson"; +import { z } from "zod"; + +export class LogsTool extends MongoDBToolBase { + protected name = "mongodb-logs"; + protected description = "Returns the most recent logged mongod events"; + protected argsShape = { + type: z + .enum(["global", "startupWarnings"]) + .optional() + .default("global") + .describe( + "The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started." + ), + limit: z + .number() + .int() + .max(1024) + .min(1) + .optional() + .default(50) + .describe("The maximum number of log entries to return."), + }; + + protected operationType: OperationType = "metadata"; + + protected async execute({ type, limit }: ToolArgs): Promise { + const provider = await this.ensureConnected(); + + const result = await provider.runCommandWithCheck("admin", { + getLog: type, + }); + + const logs = (result.log as string[]).slice(0, limit); + + return { + content: [ + { + text: `Found: ${result.totalLinesWritten} messages`, + type: "text", + }, + + ...logs.map( + (log) => + ({ + text: log, + type: "text", + }) as const + ), + ], + }; + } +} diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index eddbd26b..d64d53ea 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -17,6 +17,7 @@ import { DropDatabaseTool } from "./delete/dropDatabase.js"; import { DropCollectionTool } from "./delete/dropCollection.js"; import { ExplainTool } from "./metadata/explain.js"; import { CreateCollectionTool } from "./create/createCollection.js"; +import { LogsTool } from "./metadata/logs.js"; export const MongoDbTools = [ ConnectTool, @@ -38,4 +39,5 @@ export const MongoDbTools = [ DropCollectionTool, ExplainTool, CreateCollectionTool, + LogsTool, ]; diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index 5b7ebe1c..3c458da6 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -101,13 +101,17 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati }; } -export function getResponseContent(content: unknown): string { +export function getResponseContent(content: unknown | { content: unknown }): string { return getResponseElements(content) .map((item) => item.text) .join("\n"); } -export function getResponseElements(content: unknown): { type: string; text: string }[] { +export function getResponseElements(content: unknown | { content: unknown }): { type: string; text: string }[] { + if (typeof content === "object" && content !== null && "content" in content) { + content = (content as { content: unknown }).content; + } + expect(Array.isArray(content)).toBe(true); const response = content as { type: string; text: string }[]; diff --git a/tests/integration/tools/mongodb/delete/dropDatabase.test.ts b/tests/integration/tools/mongodb/delete/dropDatabase.test.ts index 29a79206..6293df40 100644 --- a/tests/integration/tools/mongodb/delete/dropDatabase.test.ts +++ b/tests/integration/tools/mongodb/delete/dropDatabase.test.ts @@ -21,7 +21,7 @@ describeWithMongoDB("dropDatabase tool", (integration) => { it("can drop non-existing database", async () => { let { databases } = await integration.mongoClient().db("").admin().listDatabases(); - const preDropLength = databases.length; + expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined(); await integration.connectMcpClient(); const response = await integration.mcpClient().callTool({ @@ -36,7 +36,6 @@ describeWithMongoDB("dropDatabase tool", (integration) => { ({ databases } = await integration.mongoClient().db("").admin().listDatabases()); - expect(databases).toHaveLength(preDropLength); expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined(); }); diff --git a/tests/integration/tools/mongodb/metadata/dbStats.test.ts b/tests/integration/tools/mongodb/metadata/dbStats.test.ts index 8e4a57c7..02a4e4a8 100644 --- a/tests/integration/tools/mongodb/metadata/dbStats.test.ts +++ b/tests/integration/tools/mongodb/metadata/dbStats.test.ts @@ -82,15 +82,13 @@ describeWithMongoDB("dbStats tool", (integration) => { } }); - describe("when not connected", () => { - validateAutoConnectBehavior(integration, "db-stats", () => { - return { - args: { - database: integration.randomDbName(), - collection: "foo", - }, - expectedResponse: `Statistics for database ${integration.randomDbName()}`, - }; - }); + validateAutoConnectBehavior(integration, "db-stats", () => { + return { + args: { + database: integration.randomDbName(), + collection: "foo", + }, + expectedResponse: `Statistics for database ${integration.randomDbName()}`, + }; }); }); diff --git a/tests/integration/tools/mongodb/metadata/logs.test.ts b/tests/integration/tools/mongodb/metadata/logs.test.ts new file mode 100644 index 00000000..33d05927 --- /dev/null +++ b/tests/integration/tools/mongodb/metadata/logs.test.ts @@ -0,0 +1,83 @@ +import { validateToolMetadata, validateThrowsForInvalidArguments, getResponseElements } from "../../../helpers.js"; +import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js"; + +describeWithMongoDB("logs tool", (integration) => { + validateToolMetadata(integration, "mongodb-logs", "Returns the most recent logged mongod events", [ + { + type: "string", + name: "type", + description: + "The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started.", + required: false, + }, + { + type: "integer", + name: "limit", + description: "The maximum number of log entries to return.", + required: false, + }, + ]); + + validateThrowsForInvalidArguments(integration, "mongodb-logs", [ + { extra: true }, + { type: 123 }, + { type: "something" }, + { limit: 0 }, + { limit: true }, + { limit: 1025 }, + ]); + + it("should return global logs", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "mongodb-logs", + arguments: {}, + }); + + const elements = getResponseElements(response); + + // Default limit is 50 + expect(elements.length).toBeLessThanOrEqual(51); + expect(elements[0].text).toMatch(/Found: \d+ messages/); + + for (let i = 1; i < elements.length; i++) { + const log = JSON.parse(elements[i].text); + expect(log).toHaveProperty("t"); + expect(log).toHaveProperty("msg"); + } + }); + + it("should return startupWarnings logs", async () => { + await integration.connectMcpClient(); + const response = await integration.mcpClient().callTool({ + name: "mongodb-logs", + arguments: { + type: "startupWarnings", + }, + }); + + const elements = getResponseElements(response); + expect(elements.length).toBeLessThanOrEqual(51); + for (let i = 1; i < elements.length; i++) { + const log = JSON.parse(elements[i].text); + expect(log).toHaveProperty("t"); + expect(log).toHaveProperty("msg"); + expect(log).toHaveProperty("tags"); + expect(log.tags).toContain("startupWarnings"); + } + }); + + validateAutoConnectBehavior(integration, "mongodb-logs", () => { + return { + args: { + database: integration.randomDbName(), + collection: "foo", + }, + validate: (content) => { + const elements = getResponseElements(content); + expect(elements.length).toBeLessThanOrEqual(51); + expect(elements[0].text).toMatch(/Found: \d+ messages/); + }, + }; + }); +}); From 38312ff42c2a0215033a2d07e20a648ff40636f1 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 24 Apr 2025 17:08:49 +0200 Subject: [PATCH 2/2] fix lint --- src/tools/mongodb/metadata/logs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/mongodb/metadata/logs.ts b/src/tools/mongodb/metadata/logs.ts index 434c23ca..9056aa59 100644 --- a/src/tools/mongodb/metadata/logs.ts +++ b/src/tools/mongodb/metadata/logs.ts @@ -1,7 +1,6 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { MongoDBToolBase } from "../mongodbTool.js"; import { ToolArgs, OperationType } from "../../tool.js"; -import { EJSON } from "bson"; import { z } from "zod"; export class LogsTool extends MongoDBToolBase {