-
Notifications
You must be signed in to change notification settings - Fork 103
feat: add data access tools #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0ee54ca
f042b5f
b191e43
9fd13b9
4987a6b
53d4599
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,11 @@ | ||
import path from "path"; | ||
import fs from "fs"; | ||
import { fileURLToPath } from "url"; | ||
|
||
const packageMetadata = fs.readFileSync(path.resolve("./package.json"), "utf8"); | ||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = path.dirname(__filename); | ||
|
||
const packageMetadata = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"); | ||
Comment on lines
-4
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may have regressed at some point, but |
||
const packageJson = JSON.parse(packageMetadata); | ||
|
||
export const config = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export enum ErrorCodes { | ||
NotConnectedToMongoDB = 1_000_000, | ||
InvalidParams = 1_000_001, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; | ||
import { ToolArgs } from "../tool.js"; | ||
|
||
export class CollectionIndexesTool extends MongoDBToolBase { | ||
protected name = "collection-indexes"; | ||
protected description = "Describe the indexes for a collection"; | ||
protected argsShape = DbOperationArgs; | ||
protected operationType: DbOperationType = "read"; | ||
|
||
protected async execute({ database, collection }: ToolArgs<typeof DbOperationArgs>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const indexes = await provider.getIndexes(database, collection); | ||
|
||
return { | ||
content: indexes.map((indexDefinition) => { | ||
return { | ||
text: `Field: ${indexDefinition.name}: ${JSON.stringify(indexDefinition.key)}`, | ||
type: "text", | ||
}; | ||
}), | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,75 @@ | ||||||
import { z } from "zod"; | ||||||
import { CallToolResult, McpError } from "@modelcontextprotocol/sdk/types.js"; | ||||||
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; | ||||||
import { DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; | ||||||
import { ToolArgs } from "../tool"; | ||||||
import { ErrorCodes } from "../../errors.js"; | ||||||
|
||||||
export class ConnectTool extends MongoDBToolBase { | ||||||
protected name = "connect"; | ||||||
protected description = "Connect to a MongoDB instance"; | ||||||
protected argsShape = { | ||||||
connectionStringOrClusterName: z | ||||||
.string() | ||||||
.optional() | ||||||
.describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format) or cluster name"), | ||||||
}; | ||||||
|
||||||
protected operationType: DbOperationType = "metadata"; | ||||||
|
||||||
protected async execute({ | ||||||
connectionStringOrClusterName, | ||||||
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> { | ||||||
if (!connectionStringOrClusterName) { | ||||||
// TODO: try reconnecting to the default connection | ||||||
return { | ||||||
content: [ | ||||||
{ type: "text", text: "No connection details provided." }, | ||||||
{ type: "text", text: "Please provide either a connection string or a cluster name" }, | ||||||
{ | ||||||
type: "text", | ||||||
text: "Alternatively, you can use the default deployment at mongodb://localhost:27017", | ||||||
}, | ||||||
], | ||||||
}; | ||||||
} | ||||||
|
||||||
let connectionString: string; | ||||||
|
||||||
if (typeof connectionStringOrClusterName === "string") { | ||||||
if ( | ||||||
connectionStringOrClusterName.startsWith("mongodb://") || | ||||||
connectionStringOrClusterName.startsWith("mongodb+srv://") | ||||||
) { | ||||||
connectionString = connectionStringOrClusterName; | ||||||
} else { | ||||||
// TODO: | ||||||
return { | ||||||
content: [ | ||||||
{ | ||||||
type: "text", | ||||||
text: `Connecting via cluster name not supported yet. Please provide a connection string.`, | ||||||
}, | ||||||
], | ||||||
}; | ||||||
} | ||||||
} else { | ||||||
throw new McpError(ErrorCodes.InvalidParams, "Invalid connection options"); | ||||||
} | ||||||
|
||||||
await this.connect(connectionString); | ||||||
|
||||||
return { | ||||||
content: [{ type: "text", text: `Successfully connected to ${connectionString}.` }], | ||||||
}; | ||||||
} | ||||||
|
||||||
private async connect(connectionString: string): Promise<void> { | ||||||
const provider = await NodeDriverServiceProvider.connect(connectionString, { | ||||||
productDocsLink: "https://docs.mongodb.com/todo-mcp", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The productDocsLink value appears to be a placeholder. It is recommended to update this URL to point to the correct documentation for MongoDB MCP.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
productName: "MongoDB MCP", | ||||||
}); | ||||||
|
||||||
this.mongodbState.serviceProvider = provider; | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; | ||
import { ToolArgs } from "../../tool.js"; | ||
|
||
export class InsertManyTool extends MongoDBToolBase { | ||
protected name = "insert-many"; | ||
protected description = "Insert an array of documents into a MongoDB collection"; | ||
protected argsShape = { | ||
...DbOperationArgs, | ||
documents: z | ||
.array(z.object({}).passthrough().describe("An individual MongoDB document")) | ||
.describe( | ||
"The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()" | ||
), | ||
}; | ||
protected operationType: DbOperationType = "create"; | ||
|
||
protected async execute({ | ||
database, | ||
collection, | ||
documents, | ||
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const result = await provider.insertMany(database, collection, documents); | ||
|
||
return { | ||
content: [ | ||
{ | ||
text: `Inserted \`${result.insertedCount}\` documents into collection \`${collection}\``, | ||
type: "text", | ||
}, | ||
{ | ||
text: `Inserted IDs: ${Object.values(result.insertedIds).join(", ")}`, | ||
type: "text", | ||
}, | ||
], | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; | ||
import { ToolArgs } from "../../tool.js"; | ||
|
||
export class InsertOneTool extends MongoDBToolBase { | ||
protected name = "insert-one"; | ||
protected description = "Insert a document into a MongoDB collection"; | ||
protected argsShape = { | ||
...DbOperationArgs, | ||
document: z | ||
.object({}) | ||
.passthrough() | ||
.describe( | ||
"The document to insert, matching the syntax of the document argument of db.collection.insertOne()" | ||
), | ||
}; | ||
|
||
protected operationType: DbOperationType = "create"; | ||
|
||
protected async execute({ | ||
database, | ||
collection, | ||
document, | ||
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const result = await provider.insertOne(database, collection, document); | ||
|
||
return { | ||
content: [ | ||
{ | ||
text: `Inserted document with ID \`${result.insertedId}\` into collection \`${collection}\``, | ||
type: "text", | ||
}, | ||
], | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; | ||
import { ToolArgs } from "../tool.js"; | ||
import { IndexDirection } from "mongodb"; | ||
|
||
export class CreateIndexTool extends MongoDBToolBase { | ||
protected name = "create-index"; | ||
protected description = "Create an index for a collection"; | ||
protected argsShape = { | ||
...DbOperationArgs, | ||
keys: z.record(z.string(), z.custom<IndexDirection>()).describe("The index definition"), | ||
}; | ||
|
||
protected operationType: DbOperationType = "create"; | ||
|
||
protected async execute({ database, collection, keys }: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const indexes = await provider.createIndexes(database, collection, [ | ||
{ | ||
key: keys, | ||
}, | ||
]); | ||
|
||
return { | ||
content: [ | ||
{ | ||
text: `Created the index \`${indexes[0]}\``, | ||
type: "text", | ||
}, | ||
], | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; | ||
import { ToolArgs } from "../../tool.js"; | ||
|
||
export class DeleteManyTool extends MongoDBToolBase { | ||
protected name = "delete-many"; | ||
protected description = "Removes all documents that match the filter from a MongoDB collection"; | ||
protected argsShape = { | ||
...DbOperationArgs, | ||
filter: z | ||
.object({}) | ||
.passthrough() | ||
.optional() | ||
.describe( | ||
"The query filter, specifying the deletion criteria. Matches the syntax of the filter argument of db.collection.deleteMany()" | ||
), | ||
}; | ||
protected operationType: DbOperationType = "delete"; | ||
|
||
protected async execute({ | ||
database, | ||
collection, | ||
filter, | ||
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const result = await provider.deleteMany(database, collection, filter); | ||
|
||
return { | ||
content: [ | ||
{ | ||
text: `Deleted \`${result.deletedCount}\` documents from collection \`${collection}\``, | ||
type: "text", | ||
}, | ||
], | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { DbOperationArgs, DbOperationType, MongoDBToolBase } from "../mongodbTool.js"; | ||
import { ToolArgs } from "../../tool.js"; | ||
|
||
export class DeleteOneTool extends MongoDBToolBase { | ||
protected name = "delete-one"; | ||
protected description = "Removes a single document that match the filter from a MongoDB collection"; | ||
protected argsShape = { | ||
...DbOperationArgs, | ||
filter: z | ||
.object({}) | ||
.passthrough() | ||
.optional() | ||
.describe( | ||
"The query filter, specifying the deletion criteria. Matches the syntax of the filter argument of db.collection.deleteMany()" | ||
), | ||
}; | ||
protected operationType: DbOperationType = "delete"; | ||
|
||
protected async execute({ | ||
database, | ||
collection, | ||
filter, | ||
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const result = await provider.deleteOne(database, collection, filter); | ||
|
||
return { | ||
content: [ | ||
{ | ||
text: `Deleted \`${result.deletedCount}\` documents from collection \`${collection}\``, | ||
type: "text", | ||
}, | ||
], | ||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had to add this since I was seeing complaints that node was first trying to parse it as cjs, but was seeing an esm. I don't have a strong opinion on whether we should go with one or the other.