Skip to content

Commit 4350cb3

Browse files
authored
feat: add logs tool (#114)
1 parent 543a5db commit 4350cb3

File tree

6 files changed

+155
-14
lines changed

6 files changed

+155
-14
lines changed

src/tools/mongodb/metadata/logs.ts

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

src/tools/mongodb/tools.ts

Lines changed: 2 additions & 0 deletions
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

Lines changed: 6 additions & 2 deletions
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 8 additions & 10 deletions
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
});
Lines changed: 83 additions & 0 deletions
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)