-
Notifications
You must be signed in to change notification settings - Fork 108
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 2 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,3 @@ | ||
export enum ErrorCodes { | ||
NotConnectedToMongoDB = 1_000_000, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { MongoDBToolBase } from "./mongodbTool.js"; | ||
import { ToolArgs } from "../tool.js"; | ||
|
||
const argsShape = { | ||
database: z.string().describe("Database name"), | ||
collection: z.string().describe("Collection name"), | ||
}; | ||
|
||
export class CollectionIndexesTool extends MongoDBToolBase<typeof argsShape> { | ||
protected name = "collection-indexes"; | ||
protected description = "Describe the indexes for a collection"; | ||
protected argsShape = argsShape; | ||
|
||
protected async execute({ database, collection }: ToolArgs<typeof argsShape>): 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,44 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { MongoDBToolBase } from "./mongodbTool.js"; | ||
import { ToolArgs } from "../tool.js"; | ||
import { parseSchema, SchemaField } from "mongodb-schema"; | ||
|
||
const argsShape = { | ||
database: z.string().describe("Database name"), | ||
collection: z.string().describe("Collection name"), | ||
}; | ||
|
||
export class CollectionSchemaTool extends MongoDBToolBase<typeof argsShape> { | ||
protected name = "collection-schema"; | ||
protected description = "Describe the schema for a collection"; | ||
protected argsShape = argsShape; | ||
|
||
protected async execute({ database, collection }: ToolArgs<typeof argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const documents = await provider.find(database, collection, {}, { limit: 5 }).toArray(); | ||
const schema = await parseSchema(documents); | ||
|
||
return { | ||
content: [ | ||
{ | ||
text: `Found ${schema.fields.length} fields in the schema for \`${database}.${collection}\``, | ||
type: "text", | ||
}, | ||
{ | ||
text: this.formatFieldOutput(schema.fields), | ||
type: "text", | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
private formatFieldOutput(fields: SchemaField[]): string { | ||
let result = "| Field | Type | Confidence |\n"; | ||
result += "|-------|------|-------------|\n"; | ||
for (const field of fields) { | ||
result += `| ${field.name} | \`${field.type}\` | ${(field.probability * 100).toFixed(0)}% |\n`; | ||
} | ||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,79 @@ | ||||||
import { z } from "zod"; | ||||||
import { CallToolResult, McpError } from "@modelcontextprotocol/sdk/types.js"; | ||||||
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; | ||||||
import { MongoDBToolBase } from "./mongodbTool.js"; | ||||||
import { ToolArgs } from "../tool"; | ||||||
|
||||||
const argsShape = { | ||||||
connectionStringOrClusterName: z | ||||||
.string() | ||||||
.optional() | ||||||
.describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format) or cluster name"), | ||||||
}; | ||||||
|
||||||
export class ConnectTool extends MongoDBToolBase<typeof argsShape> { | ||||||
protected name = "connect"; | ||||||
protected description = "Connect to a MongoDB instance"; | ||||||
protected argsShape = argsShape; | ||||||
|
||||||
protected async execute({ connectionStringOrClusterName }: ToolArgs<typeof argsShape>): Promise<CallToolResult> { | ||||||
try { | ||||||
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(2, "Invalid connection options"); | ||||||
} | ||||||
|
||||||
await this.connect(connectionString); | ||||||
|
||||||
return { | ||||||
content: [{ type: "text", text: `Successfully connected to ${connectionString}.` }], | ||||||
}; | ||||||
} catch (error) { | ||||||
return { | ||||||
content: [{ type: "text", text: "Failed to get cluster connection string" }], | ||||||
isError: true, | ||||||
}; | ||||||
} | ||||||
} | ||||||
|
||||||
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,35 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { MongoDBToolBase } from "./mongodbTool.js"; | ||
import { ToolArgs } from "../tool.js"; | ||
import { IndexDirection } from "mongodb"; | ||
|
||
const argsShape = { | ||
database: z.string().describe("Database name"), | ||
collection: z.string().describe("Collection name"), | ||
keys: z.record(z.string(), z.custom<IndexDirection>()).describe("The index definition"), | ||
}; | ||
|
||
export class CreateIndexTool extends MongoDBToolBase<typeof argsShape> { | ||
protected name = "create-index"; | ||
protected description = "Create an index for a collection"; | ||
protected argsShape = argsShape; | ||
|
||
protected async execute({ database, collection, keys }: ToolArgs<typeof 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,54 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { MongoDBToolBase } from "../mongodbTool.js"; | ||
import { ToolArgs } from "../../tool.js"; | ||
|
||
const argsShape = { | ||
collection: z.string().describe("Collection name"), | ||
database: z.string().describe("Database name"), | ||
filter: z | ||
.object({}) | ||
.passthrough() | ||
.optional() | ||
.describe("The query filter, matching the syntax of the query argument of db.collection.find()"), | ||
projection: z | ||
.object({}) | ||
.passthrough() | ||
.optional() | ||
.describe("The projection, matching the syntax of the projection argument of db.collection.find()"), | ||
limit: z.number().optional().default(10).describe("The maximum number of documents to return"), | ||
}; | ||
|
||
export class FindTool extends MongoDBToolBase<typeof argsShape> { | ||
protected name = "find"; | ||
protected description = "Run a find query against a MongoDB collection"; | ||
protected argsShape = argsShape; | ||
|
||
protected async execute({ | ||
database, | ||
collection, | ||
filter, | ||
projection, | ||
limit, | ||
}: ToolArgs<typeof argsShape>): Promise<CallToolResult> { | ||
const provider = this.ensureConnected(); | ||
const documents = await provider.find(database, collection, filter, { projection, limit }).toArray(); | ||
|
||
const content: Array<{ text: string; type: string }> = [ | ||
{ | ||
text: `Found ${documents.length} documents in the collection \`${collection}\``, | ||
type: "text", | ||
}, | ||
...documents.map((doc) => { | ||
return { | ||
text: `Document: \`${JSON.stringify(doc)}\``, | ||
type: "text", | ||
}; | ||
}), | ||
]; | ||
|
||
return { | ||
content: content as any, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,31 @@ | ||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
import { State } from "../../state.js"; | ||
import { ConnectTool } from "./connect.js"; | ||
import { ListCollectionsTool } from "./listCollections.js"; | ||
import { CollectionIndexesTool } from "./collectionIndexes.js"; | ||
import { ListDatabasesTool } from "./listDatabases.js"; | ||
import { MongoDBToolState } from "./mongodbTool.js"; | ||
import { CreateIndexTool } from "./createIndex.js"; | ||
import { CollectionSchemaTool } from "./collectionSchema.js"; | ||
import { InsertOneTool } from "./insert/insertOne.js"; | ||
import { FindTool } from "./find/find.js"; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
export function registerMongoDBTools(server: McpServer, state: State) {} | ||
export function registerMongoDBTools(server: McpServer, state: State) { | ||
const mongodbToolState: MongoDBToolState = {}; | ||
|
||
const tools = [ | ||
ConnectTool, | ||
ListCollectionsTool, | ||
ListDatabasesTool, | ||
CollectionIndexesTool, | ||
CreateIndexTool, | ||
CollectionSchemaTool, | ||
InsertOneTool, | ||
FindTool, | ||
]; | ||
|
||
for (const tool of tools) { | ||
const instance = new tool(state, mongodbToolState); | ||
instance.register(server); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { z } from "zod"; | ||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; | ||
import { MongoDBToolBase } from "../mongodbTool.js"; | ||
import { ToolArgs } from "../../tool.js"; | ||
|
||
const argsShape = { | ||
collection: z.string().describe("Collection name"), | ||
database: z.string().describe("Database name"), | ||
document: z | ||
.object({}) | ||
.passthrough() | ||
.describe("The document to insert, matching the syntax of the document argument of db.collection.insertOne()"), | ||
}; | ||
|
||
export class InsertOneTool extends MongoDBToolBase<typeof argsShape> { | ||
protected name = "insert-one"; | ||
protected description = "Insert a document into a MongoDB collection"; | ||
protected argsShape = argsShape; | ||
|
||
protected async execute({ database, collection, document }: ToolArgs<typeof 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", | ||
}, | ||
], | ||
}; | ||
} | ||
} |
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.