Skip to content

Commit 0ee54ca

Browse files
committed
feat: add data access tools
1 parent 9edf3ca commit 0ee54ca

17 files changed

+4526
-690
lines changed

package-lock.json

Lines changed: 4120 additions & 679 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"publishConfig": {
1515
"access": "public"
1616
},
17+
"type": "module",
1718
"scripts": {
1819
"prepare": "npm run build",
1920
"build:clean": "rm -rf dist",
@@ -40,11 +41,16 @@
4041
"globals": "^16.0.0",
4142
"prettier": "^3.5.3",
4243
"typescript": "^5.8.2",
43-
"typescript-eslint": "^8.29.1",
44-
"zod": "^3.24.2"
44+
"typescript-eslint": "^8.29.1"
4545
},
4646
"dependencies": {
47-
"@types/express": "^5.0.1"
47+
"@mongodb-js/devtools-connect": "^3.7.2",
48+
"@mongosh/service-provider-node-driver": "^3.6.0",
49+
"@types/express": "^5.0.1",
50+
"bson": "^6.10.3",
51+
"mongodb": "^6.15.0",
52+
"mongodb-schema": "^12.6.2",
53+
"zod": "^3.24.2"
4854
},
4955
"engines": {
5056
"node": ">=23.0.0"

src/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import path from "path";
22
import fs from "fs";
3+
import { fileURLToPath } from "url";
34

4-
const packageMetadata = fs.readFileSync(path.resolve("./package.json"), "utf8");
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
8+
const packageMetadata = fs.readFileSync(path.resolve(path.join(__dirname, "..", "package.json")), "utf8");
59
const packageJson = JSON.parse(packageMetadata);
610

711
export const config = {

src/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export enum ErrorCodes {
2+
NotConnectedToMongoDB = 1_000_000,
3+
}

src/tools/atlas/listClusters.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ensureAuthenticated } from "./auth.js";
55
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
66
import { AtlasToolBase } from "./atlasTool.js";
77
import { State } from "../../state.js";
8+
import { ToolArgs } from "../tool.js";
89

910
export class ListClustersTool extends AtlasToolBase<{
1011
projectId: ZodString | ZodOptional<ZodString>;
@@ -29,7 +30,7 @@ export class ListClustersTool extends AtlasToolBase<{
2930
};
3031
}
3132

32-
protected async execute({ projectId }: { projectId: string }): Promise<CallToolResult> {
33+
protected async execute({ projectId }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
3334
await ensureAuthenticated(this.state, this.apiClient);
3435

3536
let clusters: AtlasCluster[] | undefined = undefined;

src/tools/mongodb/collectionSchema.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { MongoDBToolBase } from "./mongodbTool.js";
4+
import { ToolArgs } from "../tool.js";
5+
import { parseSchema, SchemaField } from "mongodb-schema";
6+
7+
const argsShape = {
8+
database: z.string().describe("Database name"),
9+
collection: z.string().describe("Collection name"),
10+
};
11+
12+
export class CollectionSchemaTool extends MongoDBToolBase<typeof argsShape> {
13+
protected name = "collection-schema";
14+
protected description = "Describe the schema for a collection";
15+
protected argsShape = argsShape;
16+
17+
protected async execute({ database, collection }: ToolArgs<typeof argsShape>): Promise<CallToolResult> {
18+
const provider = this.ensureConnected();
19+
const documents = await provider.find(database, collection, {}, { limit: 5 }).toArray();
20+
const schema = await parseSchema(documents);
21+
22+
return {
23+
content: [
24+
{
25+
text: `Found ${schema.fields.length} fields in the schema for \`${database}.${collection}\``,
26+
type: "text",
27+
},
28+
{
29+
text: this.formatFieldOutput(schema.fields),
30+
type: "text",
31+
},
32+
],
33+
};
34+
}
35+
36+
private formatFieldOutput(fields: SchemaField[]): string {
37+
let result = "| Field | Type | Confidence |\n";
38+
result += "|-------|------|-------------|\n";
39+
for (const field of fields) {
40+
result += `| ${field.name} | \`${field.type}\` | ${(field.probability * 100).toFixed(0)}% |\n`;
41+
}
42+
return result;
43+
}
44+
}

src/tools/mongodb/connect.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { z } from "zod";
2+
import { CallToolResult, McpError } from "@modelcontextprotocol/sdk/types.js";
3+
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
4+
import { MongoDBToolBase } from "./mongodbTool.js";
5+
import { ToolArgs } from "../tool";
6+
7+
const argsShape = {
8+
connectionStringOrClusterName: z
9+
.string()
10+
.optional()
11+
.describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format) or cluster name"),
12+
};
13+
14+
export class ConnectTool extends MongoDBToolBase<typeof argsShape> {
15+
protected name = "connect";
16+
protected description = "Connect to a MongoDB instance";
17+
protected argsShape = argsShape;
18+
19+
protected async execute({ connectionStringOrClusterName }: ToolArgs<typeof argsShape>): Promise<CallToolResult> {
20+
try {
21+
if (!connectionStringOrClusterName) {
22+
// TODO: try reconnecting to the default connection
23+
return {
24+
content: [
25+
{ type: "text", text: "No connection details provided." },
26+
{ type: "text", text: "Please provide either a connection string or a cluster name" },
27+
{
28+
type: "text",
29+
text: "Alternatively, you can use the default deployment at mongodb://localhost:27017",
30+
},
31+
],
32+
};
33+
}
34+
35+
let connectionString: string;
36+
37+
if (typeof connectionStringOrClusterName === "string") {
38+
if (
39+
connectionStringOrClusterName.startsWith("mongodb://") ||
40+
connectionStringOrClusterName.startsWith("mongodb+srv://")
41+
) {
42+
connectionString = connectionStringOrClusterName;
43+
} else {
44+
// TODO:
45+
return {
46+
content: [
47+
{
48+
type: "text",
49+
text: `Connecting via cluster name not supported yet. Please provide a connection string.`,
50+
},
51+
],
52+
};
53+
}
54+
} else {
55+
throw new McpError(2, "Invalid connection options");
56+
}
57+
58+
await this.connect(connectionString);
59+
60+
return {
61+
content: [{ type: "text", text: `Successfully connected to ${connectionString}.` }],
62+
};
63+
} catch (error) {
64+
return {
65+
content: [{ type: "text", text: "Failed to get cluster connection string" }],
66+
isError: true,
67+
};
68+
}
69+
}
70+
71+
private async connect(connectionString: string): Promise<void> {
72+
const provider = await NodeDriverServiceProvider.connect(connectionString, {
73+
productDocsLink: "https://docs.mongodb.com/todo-mcp",
74+
productName: "MongoDB MCP",
75+
});
76+
77+
this.mongodbState.serviceProvider = provider;
78+
}
79+
}

src/tools/mongodb/createIndex.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { MongoDBToolBase } from "./mongodbTool.js";
4+
import { ToolArgs } from "../tool.js";
5+
import { IndexDirection } from "mongodb";
6+
7+
const argsShape = {
8+
database: z.string().describe("Database name"),
9+
collection: z.string().describe("Collection name"),
10+
keys: z.record(z.string(), z.custom<IndexDirection>()).describe("The index definition"),
11+
};
12+
13+
export class CreateIndexTool extends MongoDBToolBase<typeof argsShape> {
14+
protected name = "create-index";
15+
protected description = "Create an index for a collection";
16+
protected argsShape = argsShape;
17+
18+
protected async execute({ database, collection, keys }: ToolArgs<typeof argsShape>): Promise<CallToolResult> {
19+
const provider = this.ensureConnected();
20+
const indexes = await provider.createIndexes(database, collection, [
21+
{
22+
key: keys,
23+
},
24+
]);
25+
26+
return {
27+
content: [
28+
{
29+
text: `Created the index \`${indexes[0]}\``,
30+
type: "text",
31+
},
32+
],
33+
};
34+
}
35+
}

src/tools/mongodb/find/find.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { z } from "zod";
2+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3+
import { MongoDBToolBase } from "../mongodbTool.js";
4+
import { ToolArgs } from "../../tool.js";
5+
6+
const argsShape = {
7+
collection: z.string().describe("Collection name"),
8+
database: z.string().describe("Database name"),
9+
filter: z
10+
.object({})
11+
.passthrough()
12+
.optional()
13+
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"),
14+
projection: z
15+
.object({})
16+
.passthrough()
17+
.optional()
18+
.describe("The projection, matching the syntax of the projection argument of db.collection.find()"),
19+
limit: z.number().optional().default(10).describe("The maximum number of documents to return"),
20+
};
21+
22+
export class FindTool extends MongoDBToolBase<typeof argsShape> {
23+
protected name = "find";
24+
protected description = "Run a find query against a MongoDB collection";
25+
protected argsShape = argsShape;
26+
27+
protected async execute({
28+
database,
29+
collection,
30+
filter,
31+
projection,
32+
limit,
33+
}: ToolArgs<typeof argsShape>): Promise<CallToolResult> {
34+
const provider = this.ensureConnected();
35+
const documents = await provider.find(database, collection, filter, { projection, limit }).toArray();
36+
37+
const content: Array<{ text: string; type: string }> = [
38+
{
39+
text: `Found ${documents.length} documents in the collection \`${collection}\``,
40+
type: "text",
41+
},
42+
...documents.map((doc) => {
43+
return {
44+
text: `Document: \`${JSON.stringify(doc)}\``,
45+
type: "text",
46+
};
47+
}),
48+
];
49+
50+
return {
51+
content: content as any,
52+
};
53+
}
54+
}

src/tools/mongodb/index.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { State } from "../../state.js";
3+
import { ConnectTool } from "./connect.js";
4+
import { ListCollectionsTool } from "./listCollections.js";
5+
import { ListIndexesTool } from "./listIndexes.js";
6+
import { ListDatabasesTool } from "./listDatabases.js";
7+
import { MongoDBToolState } from "./mongodbTool.js";
8+
import { CreateIndexTool } from "./createIndex.js";
9+
import { CollectionSchemaTool } from "./collectionSchema.js";
10+
import { InsertOneTool } from "./insert/insertOne.js";
11+
import { FindTool } from "./find/find.js";
312

4-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5-
export function registerMongoDBTools(server: McpServer, state: State) {}
13+
export function registerMongoDBTools(server: McpServer, state: State) {
14+
const mongodbToolState: MongoDBToolState = {};
15+
16+
const tools = [
17+
ConnectTool,
18+
ListCollectionsTool,
19+
ListDatabasesTool,
20+
ListIndexesTool,
21+
CreateIndexTool,
22+
CollectionSchemaTool,
23+
InsertOneTool,
24+
FindTool,
25+
];
26+
27+
for (const tool of tools) {
28+
const instance = new tool(state, mongodbToolState);
29+
instance.register(server);
30+
}
31+
}

0 commit comments

Comments
 (0)