Skip to content

Commit 18671f1

Browse files
committed
feat: add logs tool
1 parent 0584f38 commit 18671f1

File tree

6 files changed

+156
-14
lines changed

6 files changed

+156
-14
lines changed

src/tools/mongodb/metadata/logs.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2+
import { MongoDBToolBase } from "../mongodbTool.js";
3+
import { ToolArgs, OperationType } from "../../tool.js";
4+
import { EJSON } from "bson";
5+
import { z } from "zod";
6+
7+
export class LogsTool extends MongoDBToolBase {
8+
protected name = "mongodb-logs";
9+
protected description = "Returns the most recent logged mongod events";
10+
protected argsShape = {
11+
type: z
12+
.enum(["global", "startupWarnings"])
13+
.optional()
14+
.default("global")
15+
.describe(
16+
"The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started."
17+
),
18+
limit: z
19+
.number()
20+
.int()
21+
.max(1024)
22+
.min(1)
23+
.optional()
24+
.default(50)
25+
.describe("The maximum number of log entries to return."),
26+
};
27+
28+
protected operationType: OperationType = "metadata";
29+
30+
protected async execute({ type, limit }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
31+
const provider = await this.ensureConnected();
32+
33+
const result = await provider.runCommandWithCheck("admin", {
34+
getLog: type,
35+
});
36+
37+
const logs = (result.log as string[]).slice(0, limit);
38+
39+
return {
40+
content: [
41+
{
42+
text: `Found: ${result.totalLinesWritten} messages`,
43+
type: "text",
44+
},
45+
46+
...logs.map(
47+
(log) =>
48+
({
49+
text: log,
50+
type: "text",
51+
}) as const
52+
),
53+
],
54+
};
55+
}
56+
}

src/tools/mongodb/tools.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DropDatabaseTool } from "./delete/dropDatabase.js";
1717
import { DropCollectionTool } from "./delete/dropCollection.js";
1818
import { ExplainTool } from "./metadata/explain.js";
1919
import { CreateCollectionTool } from "./create/createCollection.js";
20+
import { LogsTool } from "./metadata/logs.js";
2021

2122
export const MongoDbTools = [
2223
ConnectTool,
@@ -38,4 +39,5 @@ export const MongoDbTools = [
3839
DropCollectionTool,
3940
ExplainTool,
4041
CreateCollectionTool,
42+
LogsTool,
4143
];

tests/integration/helpers.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,17 @@ export function setupIntegrationTest(userConfig: UserConfig = config): Integrati
101101
};
102102
}
103103

104-
export function getResponseContent(content: unknown): string {
104+
export function getResponseContent(content: unknown | { content: unknown }): string {
105105
return getResponseElements(content)
106106
.map((item) => item.text)
107107
.join("\n");
108108
}
109109

110-
export function getResponseElements(content: unknown): { type: string; text: string }[] {
110+
export function getResponseElements(content: unknown | { content: unknown }): { type: string; text: string }[] {
111+
if (typeof content === "object" && content !== null && "content" in content) {
112+
content = (content as { content: unknown }).content;
113+
}
114+
111115
expect(Array.isArray(content)).toBe(true);
112116

113117
const response = content as { type: string; text: string }[];

tests/integration/tools/mongodb/delete/dropDatabase.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describeWithMongoDB("dropDatabase tool", (integration) => {
2121
it("can drop non-existing database", async () => {
2222
let { databases } = await integration.mongoClient().db("").admin().listDatabases();
2323

24-
const preDropLength = databases.length;
24+
expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
2525

2626
await integration.connectMcpClient();
2727
const response = await integration.mcpClient().callTool({
@@ -36,7 +36,6 @@ describeWithMongoDB("dropDatabase tool", (integration) => {
3636

3737
({ databases } = await integration.mongoClient().db("").admin().listDatabases());
3838

39-
expect(databases).toHaveLength(preDropLength);
4039
expect(databases.find((db) => db.name === integration.randomDbName())).toBeUndefined();
4140
});
4241

tests/integration/tools/mongodb/metadata/dbStats.test.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,13 @@ describeWithMongoDB("dbStats tool", (integration) => {
8282
}
8383
});
8484

85-
describe("when not connected", () => {
86-
validateAutoConnectBehavior(integration, "db-stats", () => {
87-
return {
88-
args: {
89-
database: integration.randomDbName(),
90-
collection: "foo",
91-
},
92-
expectedResponse: `Statistics for database ${integration.randomDbName()}`,
93-
};
94-
});
85+
validateAutoConnectBehavior(integration, "db-stats", () => {
86+
return {
87+
args: {
88+
database: integration.randomDbName(),
89+
collection: "foo",
90+
},
91+
expectedResponse: `Statistics for database ${integration.randomDbName()}`,
92+
};
9593
});
9694
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { validateToolMetadata, validateThrowsForInvalidArguments, getResponseElements } from "../../../helpers.js";
2+
import { describeWithMongoDB, validateAutoConnectBehavior } from "../mongodbHelpers.js";
3+
4+
describeWithMongoDB("logs tool", (integration) => {
5+
validateToolMetadata(integration, "mongodb-logs", "Returns the most recent logged mongod events", [
6+
{
7+
type: "string",
8+
name: "type",
9+
description:
10+
"The type of logs to return. Global returns all recent log entries, while startupWarnings returns only warnings and errors from when the process started.",
11+
required: false,
12+
},
13+
{
14+
type: "integer",
15+
name: "limit",
16+
description: "The maximum number of log entries to return.",
17+
required: false,
18+
},
19+
]);
20+
21+
validateThrowsForInvalidArguments(integration, "mongodb-logs", [
22+
{ extra: true },
23+
{ type: 123 },
24+
{ type: "something" },
25+
{ limit: 0 },
26+
{ limit: true },
27+
{ limit: 1025 },
28+
]);
29+
30+
it("should return global logs", async () => {
31+
await integration.connectMcpClient();
32+
const response = await integration.mcpClient().callTool({
33+
name: "mongodb-logs",
34+
arguments: {},
35+
});
36+
37+
const elements = getResponseElements(response);
38+
39+
// Default limit is 50
40+
expect(elements.length).toBeLessThanOrEqual(51);
41+
expect(elements[0].text).toMatch(/Found: \d+ messages/);
42+
43+
for (let i = 1; i < elements.length; i++) {
44+
const log = JSON.parse(elements[i].text);
45+
expect(log).toHaveProperty("t");
46+
expect(log).toHaveProperty("msg");
47+
}
48+
});
49+
50+
it("should return startupWarnings logs", async () => {
51+
await integration.connectMcpClient();
52+
const response = await integration.mcpClient().callTool({
53+
name: "mongodb-logs",
54+
arguments: {
55+
type: "startupWarnings",
56+
},
57+
});
58+
59+
const elements = getResponseElements(response);
60+
expect(elements.length).toBeLessThanOrEqual(51);
61+
for (let i = 1; i < elements.length; i++) {
62+
const log = JSON.parse(elements[i].text);
63+
expect(log).toHaveProperty("t");
64+
expect(log).toHaveProperty("msg");
65+
expect(log).toHaveProperty("tags");
66+
expect(log.tags).toContain("startupWarnings");
67+
}
68+
});
69+
70+
validateAutoConnectBehavior(integration, "mongodb-logs", () => {
71+
return {
72+
args: {
73+
database: integration.randomDbName(),
74+
collection: "foo",
75+
},
76+
validate: (content) => {
77+
const elements = getResponseElements(content);
78+
expect(elements.length).toBeLessThanOrEqual(51);
79+
expect(elements[0].text).toMatch(/Found: \d+ messages/);
80+
},
81+
};
82+
});
83+
});

0 commit comments

Comments
 (0)