From 9a6f1dfa5e2c7234ba26d3d042e5a9264e48298b Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Wed, 26 Nov 2025 21:35:06 +0100 Subject: [PATCH 1/7] chore: add developer guide and code examples to extend MCP server via library exports --- MCP_SERVER_LIBRARY.md | 973 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 973 insertions(+) create mode 100644 MCP_SERVER_LIBRARY.md diff --git a/MCP_SERVER_LIBRARY.md b/MCP_SERVER_LIBRARY.md new file mode 100644 index 00000000..25e69c91 --- /dev/null +++ b/MCP_SERVER_LIBRARY.md @@ -0,0 +1,973 @@ +# Developer's Guide to Embedding and Extending the MongoDB MCP Server + +This guide explains how to embed and extend the MongoDB MCP Server as a library to customize its core functionality and behavior for your specific use cases. + +## 📚 Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Core Concepts](#core-concepts) +- [Use Cases](#use-cases) + - [Use Case 1: Override Server Configuration](#use-case-1-override-server-configuration) + - [Use Case 2: Per-Session Configuration](#use-case-2-per-session-configuration) + - [Use Case 3: Adding Custom Tools](#use-case-3-adding-custom-tools) +- [API Reference](#api-reference) +- [Advanced Topics](#advanced-topics) +- [Examples](#examples) + +## Overview + +The MongoDB MCP Server can be embedded in your own Node.js applications and customized to meet specific requirements. The library exports provide full control over: + +- Server configuration and initialization +- Per-session(MCP Client session) configuration hooks +- Custom tool registration +- Connection management and Connection error handling + +## Installation + +Install the MongoDB MCP Server package: + +```bash +npm install mongodb-mcp-server +``` + +The package provides both CommonJS and ES Module exports. + +## Core Concepts + +### Exported Modules + +The library exports are organized in two entry points: + +**Main Library (`mongodb-mcp-server`):** + +```typescript +import { + Server, + Session, + UserConfig, + UserConfigSchema, + parseCliArgumentsAsUserConfig, + StreamableHttpRunner, + StdioRunner, + TransportRunnerBase, + LoggerBase, + Telemetry, + Keychain, + Elicitation, + MongoDBError, + ErrorCodes, + connectionErrorHandler, + createMCPConnectionManager, + applyConfigOverrides, + // ... and more +} from "mongodb-mcp-server"; +``` + +**Tools (`mongodb-mcp-server/tools`):** + +```typescript +import { ToolBase } from "mongodb-mcp-server/tools"; +``` + +For detailed documentation of these exports and their usage, see the [API Reference](#api-reference) section. + +### Architecture + +The MongoDB MCP Server library follows a modular architecture: + +- **Transport Runners**: `StdioRunner` and `StreamableHttpRunner` manage the MCP transport layer +- **Server**: Core server that wraps the MCP Server and registers tools and resources +- **Session**: Per-client(MCP Client) connection and configuration state +- **Tools**: Individual capabilities exposed to the MCP client +- **Configuration**: User configuration with override mechanisms + +## Use Cases + +### Use Case 1: Override Server Configuration + +Configure the MCP server with custom settings, such as HTTP headers for authentication before establishing session for an MCP Client. + +#### Example: Setting HTTP Headers for Authentication + +```typescript +import { StreamableHttpRunner, UserConfigSchema } from "mongodb-mcp-server"; + +// Create a custom configuration with HTTP headers +const config = UserConfigSchema.parse({ + transport: "http", + httpPort: 3000, + httpHost: "127.0.0.1", + httpHeaders: { + "x-api-key": "your-secret-api-key", + }, + // Or your own connection string + connectionString: "mongodb://localhost:27017", + // Enable read-only mode for enhanced security + readOnly: true, +}); + +// Initialize and start the server +const runner = new StreamableHttpRunner({ userConfig: config }); +await runner.start(); + +console.log(`MongoDB MCP Server listening on ${runner.serverAddress}`); +``` + +Clients connecting to this server must include the specified headers in their requests, otherwise their Session initialization request is declined. + +#### Example: Customizing Tool Availability + +```typescript +import { StdioRunner, UserConfigSchema } from "mongodb-mcp-server"; + +const config = UserConfigSchema.parse({ + transport: "stdio", + // Or your own connection string + connectionString: "mongodb://localhost:27017", + // Disable write operations + readOnly: true, + // Disable specific tool categories + disabledTools: ["atlas", "atlas-local"], + // Customize tools requiring confirmation + confirmationRequiredTools: ["find", "aggregate"], + // Set query limits + maxDocumentsPerQuery: 50, + maxBytesPerQuery: 10 * 1024 * 1024, // 10MB +}); + +const runner = new StdioRunner({ userConfig: config }); +await runner.start(); +``` + +### Use Case 2: Per-Session Configuration + +Customize configuration for each MCP client session, enabling user-specific permissions and settings. + +#### Example: User-Based Tool Permissions + +```typescript +import { + UserConfigSchema, + StreamableHttpRunner, + type TransportRunnerConfig, +} from "mongodb-mcp-server"; +import type { OperationType } from "mongodb-mcp-server/tools"; + +// Example interface for user roles and permissions +interface UserPermissions { + role: "admin" | "developer" | "analyst"; + allowedOperations: OperationType[]; + maxDocuments: number; +} + +// Mock function to fetch user permissions (replace with your auth logic) +async function getUserPermissions(userId: string): Promise { + const userDb = { + "user-123": { + role: "analyst", + allowedOperations: ["read", "metadata"], + maxDocuments: 100, + }, + "user-456": { + role: "developer", + allowedOperations: ["read", "metadata", "create", "update"], + maxDocuments: 500, + }, + "user-789": { + role: "admin", + allowedOperations: ["read", "metadata", "create", "update", "delete"], + maxDocuments: 1000, + }, + } as Record; + + return ( + userDb[userId] || { + role: "analyst", + allowedOperations: ["read"], + maxDocuments: 10, + } + ); +} + +// Base configuration for all sessions +const baseConfig = UserConfigSchema.parse({ + transport: "http", + httpPort: 3000, + httpHost: "127.0.0.1", +}); + +// Session configuration hook +const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = + async ({ userConfig, request }) => { + // Extract user ID from request headers + const userId = request?.headers?.["x-user-id"]; + + if (typeof userId !== "string") { + throw new Error("User authentication required: x-user-id header missing"); + } + + // Fetch user permissions + const permissions = await getUserPermissions(userId); + + // Build disabled tools based on permissions + const allOperations: OperationType[] = [ + "read", + "metadata", + "create", + "update", + "delete", + "connect", + ]; + + const disabledOperations = allOperations.filter( + (op) => !permissions.allowedOperations.includes(op) + ); + + // Return customized configuration for this session + return { + ...userConfig, + disabledTools: disabledOperations, + maxDocumentsPerQuery: permissions.maxDocuments, + // Analysts get read-only access + readOnly: permissions.role === "analyst", + }; + }; + +// Initialize the server with session configuration hook +const runner = new StreamableHttpRunner({ + userConfig: baseConfig, + createSessionConfig, +}); + +await runner.start(); +console.log( + `MongoDB MCP Server running with per-user permissions at ${runner.serverAddress}` +); +``` + +#### Example: Dynamic Connection String Selection + +```typescript +import { + UserConfigSchema, + StreamableHttpRunner, + type TransportRunnerConfig, +} from "mongodb-mcp-server"; + +// Connection strings for different environments +const connectionStrings = { + production: process.env.MONGODB_PRODUCTION_URI, + staging: process.env.MONGODB_STAGING_URI, + development: process.env.MONGODB_DEV_URI, +}; + +const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = + async ({ userConfig, request }) => { + // Get environment from request header + const environment = request?.headers?.[ + "x-environment" + ] as keyof typeof connectionStrings; + + if (!environment || !connectionStrings[environment]) { + throw new Error("Invalid or missing x-environment header"); + } + + return { + ...userConfig, + connectionString: connectionStrings[environment], + // Production is read-only + readOnly: environment === "production", + }; + }; + +const runner = new StreamableHttpRunner({ + userConfig: UserConfigSchema.parse({ + transport: "http", + httpPort: 3000, + httpHost: "127.0.0.1", + }), + createSessionConfig, +}); + +await runner.start(); +console.log( + `MongoDB MCP Server running with dynamic connection selection at ${runner.serverAddress}` +); +``` + +#### Example: Integration with Request Overrides + +The library supports request-level configuration overrides when `allowRequestOverrides` is enabled. You can combine this with `createSessionConfig` for fine-grained control: + +```typescript +import { + applyConfigOverrides, + UserConfigSchema, + StreamableHttpRunner, + type UserConfig, + type TransportRunnerConfig, +} from "mongodb-mcp-server"; + +// Example interface for user roles and permissions +interface UserPermissions { + role: "admin" | "developer" | "analyst"; + requestOverridesAllowed: boolean; +} + +// Mock function to fetch user permissions (replace with your auth logic) +async function getUserPermissions(userId: string): Promise { + const userDb = { + "user-123": { + role: "analyst", + requestOverridesAllowed: false, + }, + "user-456": { + role: "developer", + requestOverridesAllowed: true, + }, + "user-789": { + role: "admin", + requestOverridesAllowed: true, + }, + } as Record; + + return ( + userDb[userId] || { + role: "analyst", + requestOverridesAllowed: false, + } + ); +} + +// Base configuration for all sessions +const baseConfig = UserConfigSchema.parse({ + transport: "http", + httpPort: 3000, + httpHost: "127.0.0.1", +}); + +const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = + async ({ userConfig, request }) => { + // Extract user ID from request headers + const userId = request?.headers?.["x-user-id"]; + + if (typeof userId !== "string") { + throw new Error("User authentication required: x-user-id header missing"); + } + + // Fetch user permissions + const permissions = await getUserPermissions(userId); + + // Generate a base config based on the user permissions + const roleBasedConfig: UserConfig = { + ...baseConfig, + allowRequestOverrides: permissions.requestOverridesAllowed, + }; + + // Now attempt to apply the overrides. For roles where overrides are not + // allowed, the default override application function will throw and reject + // the initialization request. + return applyConfigOverrides({ baseConfig: roleBasedConfig, request }); + }; + +const runner = new StreamableHttpRunner({ + userConfig: UserConfigSchema.parse({ + transport: "http", + httpPort: 3000, + httpHost: "127.0.0.1", + allowRequestOverrides: true, + connectionString: process.env.MDB_MCP_CONNECTION_STRING, + }), + createSessionConfig, +}); + +await runner.start(); +console.log( + `MongoDB MCP Server running with role-based request overrides at ${runner.serverAddress}` +); +``` + +### Use Case 3: Adding Custom Tools + +Extend the MCP server with custom tools tailored to your application's needs. + +#### Example: Connection Selector Tool + +This example shows how to create a custom tool that provides users with a list of pre-configured database connections: + +```typescript +import { z } from "zod"; +import { + StdioRunner, + UserConfigSchema, + type UserConfig, +} from "mongodb-mcp-server"; +import { + ToolBase, + type ToolCategory, + type OperationType, +} from "mongodb-mcp-server/tools"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +// Define available connections +const AVAILABLE_CONNECTIONS = { + "prod-analytics": { + name: "Production Analytics", + connectionString: process.env.MONGODB_PROD_ANALYTICS_URI!, + description: "Production analytics database (read-only)", + readOnly: true, + }, + "staging-main": { + name: "Staging Main", + connectionString: process.env.MONGODB_STAGING_URI!, + description: "Staging environment database", + readOnly: false, + }, + "dev-local": { + name: "Development Local", + connectionString: "mongodb://localhost:27017/dev", + description: "Local development database", + readOnly: false, + }, +}; + +// Custom tool to list available connections. We are expecting LLM to call this +// tool to make user aware of possible connections the MCP server could be +// connected to. +class ListConnectionsTool extends ToolBase { + override name = "list-connections"; + override category = "mongodb" as ToolCategory; + override operationType = "metadata" as OperationType; + protected override description = + "Lists all available pre-configured MongoDB connections"; + protected override argsShape = {}; + + protected override async execute(): Promise { + // Ensure that we don't leak the actual connection strings to the model + // context. + const connections = Object.entries(AVAILABLE_CONNECTIONS).map( + ([id, conn]) => ({ + id, + name: conn.name, + description: conn.description, + readOnly: conn.readOnly, + }) + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(connections), + }, + ], + }; + } + + // We don't want to report any telemetry for this tool so leaving it empty. + protected override resolveTelemetryMetadata() { + return {}; + } +} + +// Custom tool to select a specific connection. Once user is made aware of list +// of connections, they can mention the name of the connection and LLM is then +// expected to call this tool with the name of the connection and the tool will +// internally connect to the pre-configured connection string. Notice how we +// never leak any connection details in the LLM context and maintain the +// effective communication using opaque connection identifiers. +class SelectConnectionTool extends ToolBase { + override name = "select-connection"; + override category = "mongodb" as ToolCategory; + override operationType = "metadata" as OperationType; + protected override description = + "Select and connect to a pre-configured MongoDB connection by ID"; + protected override argsShape = { + connectionId: z + .enum(Object.keys(AVAILABLE_CONNECTIONS) as [string, ...string[]]) + .describe("The ID of the connection to select"), + }; + + protected override async execute(args: { + connectionId: string; + }): Promise { + const { connectionId } = args; + const connection = AVAILABLE_CONNECTIONS[connectionId]; + + if (!connection) { + return { + content: [ + { + type: "text", + text: `Error: Connection '${connectionId}' not found. Use the list-connections tool to see available connections.`, + }, + ], + isError: true, + }; + } + + try { + // Disconnect from current connection if any + await this.session.disconnect(); + + // Connect to the new connection using the MongoDB MCP's own + // ConnectionManager. The inbuilt connection manager is capable of + // handling all the connection related task as long as we are able to + // provide a `ConnectionInfo` like object to connect. + await this.session.connectionManager.connect({ + connectionString: connection.connectionString, + }); + + return { + content: [ + { + type: "text", + text: `Successfully switched to connection '${ + connection.name + }' (${connectionId})${ + connection.readOnly ? " in READ-ONLY mode" : "" + }.`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Failed to switch to connection '${connectionId}': ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + isError: true, + }; + } + } + + // We don't want to report any telemetry for this tool so leaving it empty. + protected override resolveTelemetryMetadata() { + return {}; + } +} + +// Initialize the server with custom tools +const runner = new StdioRunner({ + userConfig: UserConfigSchema.parse({ + transport: "stdio", + // Don't provide a default connection string + connectionString: undefined, + // Disable existing connect tools and Atlas tools + disabledTools: ["connect", "switch-connection", "atlas"], + }), + // Add the new connection tools to list and connect to pre-configured + // connection + additionalTools: [ListConnectionsTool, SelectConnectionTool], +}); + +await runner.start(); +console.log( + `MongoDB MCP Server running with custom connection selector tools at ${runner.serverAddress}` +); +``` + +## API Reference + +### TransportRunnerConfig + +Configuration options for initializing transport runners. + +```typescript +interface TransportRunnerConfig { + /** + * Base user configuration for the server. + * + * If you wish to parse CLI arguments or ENV variables the same way as MongoDB + * MCP server does, you may use `parseCliArgumentsAsUserConfig()` exported + * through the library interface or write your own parser logic. + * + * Optionally, you can also use UserConfigSchema to create a default + * configuration - `UserConfigSchema.parse({})`. See "UserConfigSchema" + * section for details. + */ + userConfig: UserConfig; + + /** + * Custom connection manager factory (optional). + * + * Only needed if your application needs to handle MongoDB connections + * differently and outside of MongoDB MCP server. + * + * See "Custom Connection Management" section for details. + */ + createConnectionManager?: ConnectionManagerFactoryFn; + + /** + * Custom connection error handler (optional). + * + * Allows you to customize how connection errors are handled and presented to + * users. Generally required only if you're handling connections by yourself + * (using `createConnectionManager`). + * + * See "Custom Error Handling" section for details. + */ + connectionErrorHandler?: ConnectionErrorHandler; + + /** + * Custom Atlas Local client factory (optional). + * + * Allows you to generate your own Atlas client. Useful if you plan to + * connect to private Atlas API endpoints. + */ + createAtlasLocalClient?: AtlasLocalClientFactoryFn; + + /** + * Additional loggers to use (optional). + * + * Loggers specified here will receive all log events from the server. See + * "Custom Logging" section for details. + */ + additionalLoggers?: LoggerBase[]; + + /** + * Custom telemetry properties (optional). + * + * Properties added here will be included in all telemetry events. + */ + telemetryProperties?: Partial; + + /** + * Additional custom tools to register (optional). + * + * Tools specified here will be registered alongside the default MongoDB MCP + * tools. Each tool must extend the ToolBase class. + * + * See "Use Case 3: Adding Custom Tools" for examples. + */ + additionalTools?: (new (params: ToolConstructorParams) => ToolBase)[]; + + /** + * Hook to customize configuration per session (optional). + * + * Called before each session is created, allowing you to: + * - Fetch configuration from external sources (secrets managers, APIs) + * - Apply user-specific permissions and limits + * - Modify connection strings dynamically + * - Validate authentication credentials + * + * This hook is called for each new MCP client connection. + * For stdio transport, this is called once at server startup. + * For HTTP transport, this is called for each new session. + * + * @param context.userConfig - The base user configuration + * @param context.request - Request context (headers, query params) for HTTP transport + * @returns Modified user configuration for this session + * @throws Error if authentication fails or configuration is invalid + */ + createSessionConfig?: (context: { + userConfig: UserConfig; + request?: RequestContext; + }) => Promise | UserConfig; +} +``` + +### ToolBase + +Base class for implementing custom tools. + +```typescript +abstract class ToolBase { + /** Unique name for the tool */ + abstract name: string; + + /** Tool category: 'mongodb', 'atlas', or 'atlas-local' */ + abstract category: ToolCategory; + + /** Operation type: 'metadata', 'read', 'create', 'update', 'delete', or 'connect' */ + abstract operationType: OperationType; + + /** Description shown to the MCP client */ + protected abstract description: string; + + /** Zod schema for tool arguments */ + protected abstract argsShape: ZodRawShape; + + /** Execute the tool with given arguments */ + protected abstract execute( + ...args: ToolCallbackArgs + ): Promise; + + /** Resolve telemetry metadata for the tool execution */ + protected abstract resolveTelemetryMetadata( + result: CallToolResult, + ...args: Parameters> + ): TelemetryToolMetadata; + + /** Access to the session (connection, logger, etc.) */ + protected readonly session: Session; + + /** Access to the server configuration */ + protected readonly config: UserConfig; + + /** Access to the telemetry service */ + protected readonly telemetry: Telemetry; + + /** Access to the elicitation service */ + protected readonly elicitation: Elicitation; +} +``` + +### UserConfig + +Server configuration options. See the [Configuration Options](README.md#configuration-options) section in the main README for a complete list of available configuration fields. + +### UserConfigSchema + +Zod schema for validating and creating UserConfig objects with default values. This is useful when you want to create a base configuration without parsing CLI arguments or environment variables. + +```typescript +import { UserConfigSchema } from "mongodb-mcp-server"; + +// Create a config with all default values +const defaultConfig = UserConfigSchema.parse({}); + +// Create a config with some custom values, rest will be defaults +const customConfig = UserConfigSchema.parse({ + transport: "http", + httpPort: 8080, + readOnly: true, +}); +``` + +This approach ensures you get all the default values without having to specify every configuration key manually. + +### parseCliArgumentsAsUserConfig + +Utility function to parse command-line arguments and environment variables into a UserConfig object, using the same parsing logic as the MongoDB MCP server CLI. + +_Note: This is what MongoDB MCP server uses internally._ + +```typescript +function parseCliArgumentsAsUserConfig(options?: { + args?: string[]; + helpers?: CreateUserConfigHelpers; +}): UserConfig; +``` + +**Example:** + +```typescript +import { parseCliArgumentsAsUserConfig, StdioRunner } from "mongodb-mcp-server"; + +// Parse config from process.argv and environment variables +const config = parseCliArgumentsAsUserConfig(); + +const runner = new StdioRunner({ userConfig: config }); +await runner.start(); +``` + +### applyConfigOverrides + +Utility function to manually apply request-based configuration overrides. + +_Note: This is what MongoDB MCP server uses internally._ + +```typescript +function applyConfigOverrides(params: { + baseConfig: UserConfig; + request?: RequestContext; +}): UserConfig; +``` + +## Advanced Topics + +### Custom Connection Management + +You can provide a custom connection manager factory to control how the MongoDB MCP server connects to a MongoDB instance. The only use case for this is if connection handling is done differently in your application. For example, the [MongoDB extension for VS Code](https://github.com/mongodb-js/vscode/blob/f45a4c774ffc01e9aed38f6ef00224bf921d9784/src/mcp/mcpConnectionManager.ts#L30) provides its own implementation of ConnectionManager because the connection handling is done by the extension itself. + +The default connection manager factory (`createMCPConnectionManager`) is also exported if you need to use the default implementation. + +```typescript +import { + StreamableHttpRunner, + createMCPConnectionManager, +} from "mongodb-mcp-server"; +import type { ConnectionManagerFactoryFn } from "mongodb-mcp-server"; + +// Using the default connection manager (this is the default behavior) +const runner1 = new StreamableHttpRunner({ + userConfig: config, + createConnectionManager: createMCPConnectionManager, +}); + +// Or provide a custom connection manager +const customConnectionManager: ConnectionManagerFactoryFn = async ({ + logger, + userConfig, + deviceId, +}) => { + // Return a custom ConnectionManager implementation + // that could delegate to your application's existing connection logic +}; + +const runner2 = new StreamableHttpRunner({ + userConfig: config, + createConnectionManager: customConnectionManager, +}); +``` + +### Custom Error Handling + +Provide custom error handling for connection errors. The error handler receives `MongoDBError` instances with specific error codes, and can choose to handle them or let the default handler take over. + +The default connection error handler (`connectionErrorHandler`) is also exported if you need to use the default implementation. + +**Error Types:** + +The error handler receives `MongoDBError` instances with one of the following error codes: + +- `ErrorCodes.NotConnectedToMongoDB` - Thrown when a tool requires a connection but none exists +- `ErrorCodes.MisconfiguredConnectionString` - Thrown when the connection string provided through `UserConfig` is invalid + +**ConnectionErrorHandlerContext:** + +```typescript +interface ConnectionErrorHandlerContext { + /** List of all available tools that can be suggested to the user */ + availableTools: ToolBase[]; + /** Current state of the connection manager */ + connectionState: AnyConnectionState; +} +``` + +**Example:** + +```typescript +import { + StreamableHttpRunner, + UserConfigSchema, + ErrorCodes, + connectionErrorHandler as defaultConnectionErrorHandler, +} from "mongodb-mcp-server"; +import type { ConnectionErrorHandler } from "mongodb-mcp-server"; + +// Using the default error handler (this is the default behavior) +const runner1 = new StreamableHttpRunner({ + userConfig: UserConfigSchema.parse({}), + connectionErrorHandler: defaultConnectionErrorHandler, +}); + +// Or provide a custom error handler +const customErrorHandler: ConnectionErrorHandler = (error, context) => { + // error is a MongoDBError with specific error codes + console.error("Connection error:", error.code, error.message); + + // Access available tools and connection state + const connectTools = context.availableTools.filter( + (t) => t.operationType === "connect" + ); + + if (error.code === ErrorCodes.NotConnectedToMongoDB) { + // Provide custom error message + return { + errorHandled: true, + result: { + content: [ + { + type: "text", + text: "Please connect to MongoDB first using one of the available connect tools.", + }, + ], + isError: true, + }, + }; + } + + // Delegate to default handler for other errors + return defaultConnectionErrorHandler(error, context); +}; + +const runner2 = new StreamableHttpRunner({ + userConfig: config, + connectionErrorHandler: customErrorHandler, +}); +``` + +### Custom Logging + +Add custom loggers to capture server events: + +```typescript +import { + StreamableHttpRunner, + LoggerBase, + UserConfigSchema, + type LogPayload, +} from "mongodb-mcp-server"; + +class CustomLogger extends LoggerBase { + log(payload: LogPayload): void { + // Send to your logging service + console.log(`[${payload.id}] ${payload.message}`); + } + + // Implement other log level methods... +} + +const runner = new StreamableHttpRunner({ + userConfig: UserConfigSchema.parse({}), + additionalLoggers: [new CustomLogger()], +}); +``` + +## Examples + +For complete working examples of embedding and extending the MongoDB MCP Server, refer to: + +- **Use Case Examples**: See the detailed examples in the [Use Cases](#use-cases) section above +- **MongoDB VS Code Extension**: Real-world integration of MongoDB MCP server in our extension at [mongodb-js/vscode](https://github.com/mongodb-js/vscode) +- **Custom Tools**: The [Use Case 3: Adding Custom Tools](#use-case-3-adding-custom-tools) section demonstrates creating custom connection selector tools + +## Best Practices + +### Security + +1. **Never expose connection strings or API credentials in logs or error messages** +2. **Apply the principle of least privilege when creating session configuration** +3. **Ensure only expected HTTP header and query parameters overrides are applied** +4. **Validate all inputs in custom tools** + +### Performance + +1. **Set appropriate `maxDocumentsPerQuery` and `maxBytesPerQuery` limits as this might affect runtime memory usage** +2. **Use `indexCheck: true` to ensure only indexed queries are run by the server** + +### Development + +1. **Test custom tools thoroughly before deployment** +2. **Implement comprehensive error handling in custom tools** + +## Troubleshooting + +### Common Issues + +**Problem:** Custom tools not appearing in the tool list + +- **Solution:** Ensure the tool class extends `ToolBase` and is passed in `additionalTools` +- **Solution:** Check that the tool's `verifyAllowed()` returns true and the tool is not accidentally disabled by config (disabledTools) + +**Problem:** Configuration overrides not working + +- **Solution:** Enable `allowRequestOverrides: true` in the base configuration +- **Solution:** Check that the configuration field allows overrides (see `overrideBehavior` in schema) + +**Problem:** Tool name collision error + +- **Solution:** Ensure your custom tools have unique names that don't conflict with built-in tools +- **Solution:** Check the list of built-in tool names in the [Supported Tools](README.md#supported-tools) section + +## Support + +For issues, questions, or contributions, please refer to the main [Contributing Guide](CONTRIBUTING.md) and open an issue on [GitHub](https://github.com/mongodb-js/mongodb-mcp-server). From 79fe868616d6874c40e2556f218d1109e5844254 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 1 Dec 2025 14:41:41 +0100 Subject: [PATCH 2/7] chore: bring back static operationType This commit brings back static operationType property on ToolClass declarations and ensures that current tools abides by it. Additionally we are now exporting Tools as a record instead of list to allow selective picking. --- MCP_SERVER_LIBRARY.md | 399 ++++++++++++------ knip.json | 9 +- src/lib.ts | 17 +- src/server.ts | 26 +- src/tools/atlas/connect/connectCluster.ts | 2 +- src/tools/atlas/create/createAccessList.ts | 2 +- src/tools/atlas/create/createDBUser.ts | 2 +- src/tools/atlas/create/createFreeCluster.ts | 2 +- src/tools/atlas/create/createProject.ts | 2 +- src/tools/atlas/read/getPerformanceAdvisor.ts | 2 +- src/tools/atlas/read/inspectAccessList.ts | 2 +- src/tools/atlas/read/inspectCluster.ts | 2 +- src/tools/atlas/read/listAlerts.ts | 2 +- src/tools/atlas/read/listClusters.ts | 2 +- src/tools/atlas/read/listDBUsers.ts | 2 +- src/tools/atlas/read/listOrgs.ts | 2 +- src/tools/atlas/read/listProjects.ts | 2 +- src/tools/atlas/tools.ts | 42 +- .../atlasLocal/connect/connectDeployment.ts | 2 +- .../atlasLocal/create/createDeployment.ts | 2 +- .../atlasLocal/delete/deleteDeployment.ts | 2 +- src/tools/atlasLocal/read/listDeployments.ts | 2 +- src/tools/atlasLocal/tools.ts | 10 +- src/tools/index.ts | 15 +- src/tools/mongodb/connect/connect.ts | 10 +- src/tools/mongodb/connect/switchConnection.ts | 10 +- src/tools/mongodb/create/createCollection.ts | 2 +- src/tools/mongodb/create/createIndex.ts | 2 +- src/tools/mongodb/create/insertMany.ts | 38 +- src/tools/mongodb/delete/deleteMany.ts | 2 +- src/tools/mongodb/delete/dropCollection.ts | 2 +- src/tools/mongodb/delete/dropDatabase.ts | 2 +- src/tools/mongodb/delete/dropIndex.ts | 2 +- .../mongodb/metadata/collectionIndexes.ts | 2 +- .../mongodb/metadata/collectionSchema.ts | 2 +- .../mongodb/metadata/collectionStorageSize.ts | 2 +- src/tools/mongodb/metadata/dbStats.ts | 2 +- src/tools/mongodb/metadata/explain.ts | 2 +- src/tools/mongodb/metadata/listCollections.ts | 2 +- src/tools/mongodb/metadata/listDatabases.ts | 2 +- src/tools/mongodb/metadata/logs.ts | 2 +- src/tools/mongodb/read/aggregate.ts | 2 +- src/tools/mongodb/read/count.ts | 2 +- src/tools/mongodb/read/export.ts | 2 +- src/tools/mongodb/read/find.ts | 2 +- src/tools/mongodb/tools.ts | 72 +--- src/tools/mongodb/update/renameCollection.ts | 2 +- src/tools/mongodb/update/updateMany.ts | 2 +- src/tools/tool.ts | 342 ++++++++++++++- src/transports/base.ts | 148 ++++++- tests/integration/customTools.test.ts | 4 +- .../tools/mongodb/mongodbTool.test.ts | 20 +- tests/unit/toolBase.test.ts | 3 +- 53 files changed, 909 insertions(+), 330 deletions(-) diff --git a/MCP_SERVER_LIBRARY.md b/MCP_SERVER_LIBRARY.md index 25e69c91..8d5c5240 100644 --- a/MCP_SERVER_LIBRARY.md +++ b/MCP_SERVER_LIBRARY.md @@ -11,6 +11,7 @@ This guide explains how to embed and extend the MongoDB MCP Server as a library - [Use Case 1: Override Server Configuration](#use-case-1-override-server-configuration) - [Use Case 2: Per-Session Configuration](#use-case-2-per-session-configuration) - [Use Case 3: Adding Custom Tools](#use-case-3-adding-custom-tools) + - [Use Case 4: Selective Tool Registration](#use-case-4-selective-tool-registration) - [API Reference](#api-reference) - [Advanced Topics](#advanced-topics) - [Examples](#examples) @@ -20,8 +21,8 @@ This guide explains how to embed and extend the MongoDB MCP Server as a library The MongoDB MCP Server can be embedded in your own Node.js applications and customized to meet specific requirements. The library exports provide full control over: - Server configuration and initialization -- Per-session(MCP Client session) configuration hooks -- Custom tool registration +- Per-session (MCP Client session) configuration hooks +- Tool registration - Connection management and Connection error handling ## Installation @@ -68,7 +69,14 @@ import { **Tools (`mongodb-mcp-server/tools`):** ```typescript -import { ToolBase } from "mongodb-mcp-server/tools"; +import { + ToolBase, + AllTools, + MongoDbTools, + AtlasTools, + AtlasLocalTools, + type ToolClass, +} from "mongodb-mcp-server/tools"; ``` For detailed documentation of these exports and their usage, see the [API Reference](#api-reference) section. @@ -79,7 +87,7 @@ The MongoDB MCP Server library follows a modular architecture: - **Transport Runners**: `StdioRunner` and `StreamableHttpRunner` manage the MCP transport layer - **Server**: Core server that wraps the MCP Server and registers tools and resources -- **Session**: Per-client(MCP Client) connection and configuration state +- **Session**: Per-client (MCP Client) connection and configuration state - **Tools**: Individual capabilities exposed to the MCP client - **Configuration**: User configuration with override mechanisms @@ -350,8 +358,12 @@ const baseConfig = UserConfigSchema.parse({ const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async ({ userConfig, request }) => { + if (!request) { + throw new Error("User authentication required: no headers provided"); + } + // Extract user ID from request headers - const userId = request?.headers?.["x-user-id"]; + const userId = request.headers?.["x-user-id"]; if (typeof userId !== "string") { throw new Error("User authentication required: x-user-id header missing"); @@ -377,6 +389,15 @@ const runner = new StreamableHttpRunner({ transport: "http", httpPort: 3000, httpHost: "127.0.0.1", + // For this particular example, setting `allowRequestOverrides` here is + // optional because if you notice the session configuration hook, we're + // constructing the `roleBasedConfig` with the appropriate value of + // `allowRequestOverrides` already before calling the exported + // `applyConfigOverrides` function. + // + // Here we still pass it anyways to show an example that the + // `allowRequestOverrides` can also be statically turned on during server + // initialization. allowRequestOverrides: true, connectionString: process.env.MDB_MCP_CONNECTION_STRING, }), @@ -438,8 +459,8 @@ const AVAILABLE_CONNECTIONS = { // connected to. class ListConnectionsTool extends ToolBase { override name = "list-connections"; - override category = "mongodb" as ToolCategory; - override operationType = "metadata" as OperationType; + override category: ToolCategory = "mongodb"; + static operationType: OperationType = "metadata"; protected override description = "Lists all available pre-configured MongoDB connections"; protected override argsShape = {}; @@ -480,8 +501,8 @@ class ListConnectionsTool extends ToolBase { // effective communication using opaque connection identifiers. class SelectConnectionTool extends ToolBase { override name = "select-connection"; - override category = "mongodb" as ToolCategory; - override operationType = "metadata" as OperationType; + override category: ToolCategory = "mongodb"; + static operationType: OperationType = "metadata"; protected override description = "Select and connect to a pre-configured MongoDB connection by ID"; protected override argsShape = { @@ -553,18 +574,21 @@ class SelectConnectionTool extends ToolBase { } } -// Initialize the server with custom tools +// Initialize the server with custom tools alongside internal tools const runner = new StdioRunner({ userConfig: UserConfigSchema.parse({ transport: "stdio", // Don't provide a default connection string connectionString: undefined, - // Disable existing connect tools and Atlas tools - disabledTools: ["connect", "switch-connection", "atlas"], }), - // Add the new connection tools to list and connect to pre-configured - // connection - additionalTools: [ListConnectionsTool, SelectConnectionTool], + // Register all internal tools except the default connect tools, plus our custom tools + tools: [ + ...Object.values(AllTools).filter( + (tool) => tool.operationType !== "connect" + ), + ListConnectionsTool, + SelectConnectionTool, + ], }); await runner.start(); @@ -573,150 +597,243 @@ console.log( ); ``` -## API Reference +### Use Case 4: Selective Tool Registration -### TransportRunnerConfig +Register only specific internal MongoDB tools alongside custom tools, giving you complete control over the available toolset. + +#### Example: Minimal Toolset with Custom Integration -Configuration options for initializing transport runners. +This example shows how to selectively enable only specific MongoDB tools (`aggregate`, `connect`, and `switch-connection`) while disabling all others, and adding a custom tool for application-specific functionality: ```typescript -interface TransportRunnerConfig { - /** - * Base user configuration for the server. - * - * If you wish to parse CLI arguments or ENV variables the same way as MongoDB - * MCP server does, you may use `parseCliArgumentsAsUserConfig()` exported - * through the library interface or write your own parser logic. - * - * Optionally, you can also use UserConfigSchema to create a default - * configuration - `UserConfigSchema.parse({})`. See "UserConfigSchema" - * section for details. - */ - userConfig: UserConfig; - - /** - * Custom connection manager factory (optional). - * - * Only needed if your application needs to handle MongoDB connections - * differently and outside of MongoDB MCP server. - * - * See "Custom Connection Management" section for details. - */ - createConnectionManager?: ConnectionManagerFactoryFn; - - /** - * Custom connection error handler (optional). - * - * Allows you to customize how connection errors are handled and presented to - * users. Generally required only if you're handling connections by yourself - * (using `createConnectionManager`). - * - * See "Custom Error Handling" section for details. - */ - connectionErrorHandler?: ConnectionErrorHandler; - - /** - * Custom Atlas Local client factory (optional). - * - * Allows you to generate your own Atlas client. Useful if you plan to - * connect to private Atlas API endpoints. - */ - createAtlasLocalClient?: AtlasLocalClientFactoryFn; - - /** - * Additional loggers to use (optional). - * - * Loggers specified here will receive all log events from the server. See - * "Custom Logging" section for details. - */ - additionalLoggers?: LoggerBase[]; - - /** - * Custom telemetry properties (optional). - * - * Properties added here will be included in all telemetry events. - */ - telemetryProperties?: Partial; - - /** - * Additional custom tools to register (optional). - * - * Tools specified here will be registered alongside the default MongoDB MCP - * tools. Each tool must extend the ToolBase class. - * - * See "Use Case 3: Adding Custom Tools" for examples. - */ - additionalTools?: (new (params: ToolConstructorParams) => ToolBase)[]; - - /** - * Hook to customize configuration per session (optional). - * - * Called before each session is created, allowing you to: - * - Fetch configuration from external sources (secrets managers, APIs) - * - Apply user-specific permissions and limits - * - Modify connection strings dynamically - * - Validate authentication credentials - * - * This hook is called for each new MCP client connection. - * For stdio transport, this is called once at server startup. - * For HTTP transport, this is called for each new session. - * - * @param context.userConfig - The base user configuration - * @param context.request - Request context (headers, query params) for HTTP transport - * @returns Modified user configuration for this session - * @throws Error if authentication fails or configuration is invalid - */ - createSessionConfig?: (context: { - userConfig: UserConfig; - request?: RequestContext; - }) => Promise | UserConfig; +import { z } from "zod"; +import { StreamableHttpRunner, UserConfigSchema } from "mongodb-mcp-server"; +import { + type ToolCategory, + type OperationType, + AllTools, + ToolBase, +} from "mongodb-mcp-server/tools"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +// Custom tool to fetch ticket details from your application +class GetTicketDetailsTool extends ToolBase { + override name = "get-ticket-details"; + override category: ToolCategory = "mongodb"; + static operationType: OperationType = "read"; + + protected override description = + "Retrieves detailed information about a support ticket from the tickets collection"; + + protected override argsShape = { + ticketId: z.string().describe("The unique identifier of the ticket"), + }; + + protected override async execute(args: { + ticketId: string; + }): Promise { + const { ticketId } = args; + + try { + // Ensure connected to MongoDB + await this.session.ensureConnected(); + + // Fetch ticket from the database + const ticket = await this.session.db + .collection("tickets") + .findOne({ ticketId }); + + if (!ticket) { + return { + content: [ + { + type: "text", + text: `No ticket found with ID: ${ticketId}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text: JSON.stringify(ticket, null, 2), + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error fetching ticket: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + ], + isError: true, + }; + } + } + + protected override resolveTelemetryMetadata() { + return {}; + } } + +// Select only the specific internal tools we want to keep +const selectedInternalTools = [ + AllTools.AggregateTool, + AllTools.ConnectTool, + AllTools.SwitchConnectionTool, +]; + +// Initialize the server with minimal toolset +const runner = new StreamableHttpRunner({ + userConfig: UserConfigSchema.parse({ + transport: "http", + httpPort: 3000, + httpHost: "127.0.0.1", + connectionString: process.env.MDB_MCP_CONNECTION_STRING, + }), + // Register only selected internal tools plus our custom tool + tools: [...selectedInternalTools, GetTicketDetailsTool], +}); + +await runner.start(); +console.log("MongoDB MCP Server running with minimal toolset"); ``` +In this configuration: + +- The server will **only** register three internal MongoDB tools: `aggregate`, `connect`, and `switch-connection` +- All other internal tools (find, insert, update, etc.) are not registered at all +- The custom `get-ticket-details` tool provides application-specific functionality +- Atlas and Atlas Local tools are not registered since they're not in the `tools` array + +This approach is useful when you want to: + +- Create a focused MCP server for a particular use case +- Limit LLM capabilities to specific operations +- Combine selective internal tools with domain-specific custom tools + +## API Reference + +### TransportRunnerConfig + +Configuration options for initializing transport runners (`StdioRunner`, `StreamableHttpRunner`). + +See the TypeScript definition in [`src/transports/base.ts`](./src/transports/base.ts) for detailed documentation of all available options. + ### ToolBase -Base class for implementing custom tools. +Base class for implementing custom MCP tools. + +See the TypeScript documentation in [`src/tools/tool.ts`](./src/tools/tool.ts) for: + +- Detailed explanation of `ToolBase` abstract class +- Documentation of all available protected members +- Information about required abstract properties (`name`, `category`) and required static property (`operationType`) + +**Important:** All custom tools must conform to the `ToolClass` type, which requires: + +- A **static** `operationType` property (not an instance property) +- Implementation of all abstract members from `ToolBase` + +### ToolClass + +The type that all tool classes must conform to when implementing custom tools. + +This type enforces that tool classes have: + +- A constructor that accepts `ToolConstructorParams` +- A **static** `operationType` property + +The static `operationType` is automatically injected as an instance property during tool construction by the server. + +See the TypeScript documentation in [`src/tools/tool.ts`](./src/tools/tool.ts) for complete details and examples. + +### Tool Collections + +The library exports collections of internal tool classes that can be used for selective tool registration or extension. + +#### AllTools + +An object containing all internal tool classes (MongoDB, Atlas, and Atlas Local tools combined). ```typescript -abstract class ToolBase { - /** Unique name for the tool */ - abstract name: string; +import { AllTools, MongoDbTools, AtlasTools } from "mongodb-mcp-server/tools"; + +// Pick a specific tool +const MyTool = AllTools.AggregateTool; + +// Create a list of hand picked tools +const selectedInternalTools = [ + AllTools.AggregateTool, + AllTools.ConnectTool, + AllTools.SwitchConnectionTool, +]; + +// Create a list of all internal tools except a few +const filteredTools = Object.values(AllTools).filter( + (tool) => + tool !== AllTools.ConnectTool && tool !== AllTools.SwitchConnectionTool +); - /** Tool category: 'mongodb', 'atlas', or 'atlas-local' */ - abstract category: ToolCategory; +// Filter tools by operationType (static property) +const connectionRelatedTools = Object.values(AllTools).filter( + (tool) => tool.operationType === "connect" +); +``` - /** Operation type: 'metadata', 'read', 'create', 'update', 'delete', or 'connect' */ - abstract operationType: OperationType; +#### MongoDbTools - /** Description shown to the MCP client */ - protected abstract description: string; +An object containing only MongoDB-specific tool classes (tools that interact with MongoDB deployments). - /** Zod schema for tool arguments */ - protected abstract argsShape: ZodRawShape; +```typescript +import { MongoDbTools } from "mongodb-mcp-server/tools"; - /** Execute the tool with given arguments */ - protected abstract execute( - ...args: ToolCallbackArgs - ): Promise; +// Get all MongoDB tools as an array +const mongoTools = Object.values(MongoDbTools); - /** Resolve telemetry metadata for the tool execution */ - protected abstract resolveTelemetryMetadata( - result: CallToolResult, - ...args: Parameters> - ): TelemetryToolMetadata; +// You can check static properties like operationType +const readOnlyMongoTools = mongoTools.filter( + (tool) => tool.operationType === "read" || tool.operationType === "metadata" +); +``` - /** Access to the session (connection, logger, etc.) */ - protected readonly session: Session; +#### AtlasTools - /** Access to the server configuration */ - protected readonly config: UserConfig; +An object containing only MongoDB Atlas-specific tool classes (tools that interact with Atlas API). - /** Access to the telemetry service */ - protected readonly telemetry: Telemetry; +```typescript +import { AtlasTools } from "mongodb-mcp-server/tools"; - /** Access to the elicitation service */ - protected readonly elicitation: Elicitation; -} +// Get all Atlas tools as an array +const atlasTools = Object.values(AtlasTools); + +// You can check static properties like operationType +const atlasCreationTools = atlasTools.filter( + (tool) => tool.operationType === "create" +); +``` + +#### AtlasLocalTools + +An object containing only Atlas Local-specific tool classes (tools that interact with local Atlas deployments). + +```typescript +import { AtlasLocalTools } from "mongodb-mcp-server/tools"; + +// Get all Atlas Local tools as an array +const atlasLocalTools = Object.values(AtlasLocalTools); + +// You can check static properties like operationType +const atlasLocalConnectionTools = atlasLocalTools.filter( + (tool) => tool.operationType === "connect" +); ``` ### UserConfig @@ -781,6 +898,8 @@ function applyConfigOverrides(params: { }): UserConfig; ``` +See "Example: Integration with Request Overrides" for further details on how to use this function. + ## Advanced Topics ### Custom Connection Management @@ -928,7 +1047,6 @@ For complete working examples of embedding and extending the MongoDB MCP Server, - **Use Case Examples**: See the detailed examples in the [Use Cases](#use-cases) section above - **MongoDB VS Code Extension**: Real-world integration of MongoDB MCP server in our extension at [mongodb-js/vscode](https://github.com/mongodb-js/vscode) -- **Custom Tools**: The [Use Case 3: Adding Custom Tools](#use-case-3-adding-custom-tools) section demonstrates creating custom connection selector tools ## Best Practices @@ -955,7 +1073,8 @@ For complete working examples of embedding and extending the MongoDB MCP Server, **Problem:** Custom tools not appearing in the tool list -- **Solution:** Ensure the tool class extends `ToolBase` and is passed in `additionalTools` +- **Solution:** Ensure the tool class extends `ToolBase` and is passed in the `tools` array +- **Solution:** If you want both internal and custom tools, spread `AllTools` in the array: `tools: [...Object.values(AllTools), MyCustomTool]` - **Solution:** Check that the tool's `verifyAllowed()` returns true and the tool is not accidentally disabled by config (disabledTools) **Problem:** Configuration overrides not working diff --git a/knip.json b/knip.json index 93b338ed..760eeb89 100644 --- a/knip.json +++ b/knip.json @@ -6,7 +6,14 @@ "scripts/**/*.ts", "eslint-rules/*.js" ], - "ignore": ["tests/integration/fixtures/curl.mjs", "tests/vitest.d.ts"], + "ignore": [ + // Knip, for some reason, is not able to link the exported tools to the + // final exports in `tools/index.ts` and complains about unused exports. For + // that reason we're ignoring the tool definitions. + "src/tools/**/*.ts", + "tests/integration/fixtures/curl.mjs", + "tests/vitest.d.ts" + ], "ignoreDependencies": ["@mongodb-js/atlas-local"], "ignoreExportsUsedInFile": true } diff --git a/src/lib.ts b/src/lib.ts index babdbde7..3b05eb54 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,6 +1,7 @@ export { Server, type ServerOptions } from "./server.js"; export { Session, type SessionOptions } from "./common/session.js"; -export { type UserConfig } from "./common/config/userConfig.js"; +export { type UserConfig, UserConfigSchema } from "./common/config/userConfig.js"; +export { createUserConfig as parseCliArgumentsAsUserConfig } from "./common/config/createUserConfig.js"; export { LoggerBase, type LogPayload, type LoggerType, type LogLevel } from "./common/logger.js"; export { StreamableHttpRunner } from "./transports/streamableHttp.js"; export { StdioRunner } from "./transports/stdio.js"; @@ -8,19 +9,21 @@ export { TransportRunnerBase, type TransportRunnerConfig } from "./transports/ba export { ConnectionManager, ConnectionStateConnected, + createMCPConnectionManager, type AnyConnectionState, type ConnectionState, type ConnectionStateDisconnected, type ConnectionStateErrored, type ConnectionManagerFactoryFn, } from "./common/connectionManager.js"; -export type { - ConnectionErrorHandler, - ConnectionErrorHandled, - ConnectionErrorUnhandled, - ConnectionErrorHandlerContext, +export { + connectionErrorHandler, + type ConnectionErrorHandler, + type ConnectionErrorHandled, + type ConnectionErrorUnhandled, + type ConnectionErrorHandlerContext, } from "./common/connectionErrorHandler.js"; -export { ErrorCodes } from "./common/errors.js"; +export { ErrorCodes, MongoDBError } from "./common/errors.js"; export { Telemetry } from "./telemetry/telemetry.js"; export { Keychain, registerGlobalSecretToRedact } from "./common/keychain.js"; export type { Secret } from "./common/keychain.js"; diff --git a/src/server.ts b/src/server.ts index 3c99b376..9fbe2ecb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,7 +16,7 @@ import { UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import assert from "assert"; -import type { ToolBase, ToolCategory, ToolConstructorParams } from "./tools/tool.js"; +import type { ToolBase, ToolCategory, ToolClass } from "./tools/tool.js"; import { validateConnectionString } from "./helpers/connectionOptions.js"; import { packageInfo } from "./common/packageInfo.js"; import { type ConnectionErrorHandler } from "./common/connectionErrorHandler.js"; @@ -35,10 +35,19 @@ export interface ServerOptions { * This will override any default tools. You can use both existing and custom tools by using the `mongodb-mcp-server/tools` export. * * ```ts - * import { AllTools, ToolBase } from "mongodb-mcp-server/tools"; + * import { AllTools, ToolBase, type ToolCategory, type OperationType } from "mongodb-mcp-server/tools"; * class CustomTool extends ToolBase { - * name = "custom_tool"; - * // ... + * override name = "custom_tool"; + * override category: ToolCategory = "mongodb"; + * static operationType: OperationType = "read"; + * protected description = "Custom tool description"; + * protected argsShape = {}; + * protected async execute() { + * return { content: [{ type: "text", text: "Result" }] }; + * } + * protected resolveTelemetryMetadata() { + * return {}; + * } * } * const server = new Server({ * session: mySession, @@ -47,11 +56,11 @@ export interface ServerOptions { * telemetry: myTelemetry, * elicitation: myElicitation, * connectionErrorHandler: myConnectionErrorHandler, - * tools: [...AllTools, CustomTool], + * tools: [...Object.values(AllTools), CustomTool], * }); * ``` */ - tools?: (new (params: ToolConstructorParams) => ToolBase)[]; + tools?: ToolClass[]; } export class Server { @@ -60,7 +69,7 @@ export class Server { private readonly telemetry: Telemetry; public readonly userConfig: UserConfig; public readonly elicitation: Elicitation; - private readonly toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[]; + private readonly toolConstructors: ToolClass[]; public readonly tools: ToolBase[] = []; public readonly connectionErrorHandler: ConnectionErrorHandler; @@ -89,7 +98,7 @@ export class Server { this.userConfig = userConfig; this.elicitation = elicitation; this.connectionErrorHandler = connectionErrorHandler; - this.toolConstructors = tools ?? AllTools; + this.toolConstructors = tools ?? Object.values(AllTools); } async connect(transport: Transport): Promise { @@ -242,6 +251,7 @@ export class Server { private registerTools(): void { for (const toolConstructor of this.toolConstructors) { const tool = new toolConstructor({ + operationType: toolConstructor.operationType, session: this.session, config: this.userConfig, telemetry: this.telemetry, diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 96a0a51c..bbdecd3a 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -32,7 +32,7 @@ export const ConnectClusterArgs = { export class ConnectClusterTool extends AtlasToolBase { public name = "atlas-connect-cluster"; protected description = "Connect to MongoDB Atlas cluster"; - public operationType: OperationType = "connect"; + static operationType: OperationType = "connect"; protected argsShape = { ...ConnectClusterArgs, }; diff --git a/src/tools/atlas/create/createAccessList.ts b/src/tools/atlas/create/createAccessList.ts index 2bf1649b..35f11466 100644 --- a/src/tools/atlas/create/createAccessList.ts +++ b/src/tools/atlas/create/createAccessList.ts @@ -19,7 +19,7 @@ export const CreateAccessListArgs = { export class CreateAccessListTool extends AtlasToolBase { public name = "atlas-create-access-list"; protected description = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters."; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected argsShape = { ...CreateAccessListArgs, }; diff --git a/src/tools/atlas/create/createDBUser.ts b/src/tools/atlas/create/createDBUser.ts index c8e8ea01..c6002159 100644 --- a/src/tools/atlas/create/createDBUser.ts +++ b/src/tools/atlas/create/createDBUser.ts @@ -36,7 +36,7 @@ export const CreateDBUserArgs = { export class CreateDBUserTool extends AtlasToolBase { public name = "atlas-create-db-user"; protected description = "Create an MongoDB Atlas database user"; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected argsShape = { ...CreateDBUserArgs, }; diff --git a/src/tools/atlas/create/createFreeCluster.ts b/src/tools/atlas/create/createFreeCluster.ts index 6b1ac98e..5412ef17 100644 --- a/src/tools/atlas/create/createFreeCluster.ts +++ b/src/tools/atlas/create/createFreeCluster.ts @@ -8,7 +8,7 @@ import { AtlasArgs } from "../../args.js"; export class CreateFreeClusterTool extends AtlasToolBase { public name = "atlas-create-free-cluster"; protected description = "Create a free MongoDB Atlas cluster"; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected argsShape = { projectId: AtlasArgs.projectId().describe("Atlas project ID to create the cluster in"), name: AtlasArgs.clusterName().describe("Name of the cluster"), diff --git a/src/tools/atlas/create/createProject.ts b/src/tools/atlas/create/createProject.ts index 3ce9f025..490adc04 100644 --- a/src/tools/atlas/create/createProject.ts +++ b/src/tools/atlas/create/createProject.ts @@ -7,7 +7,7 @@ import { AtlasArgs } from "../../args.js"; export class CreateProjectTool extends AtlasToolBase { public name = "atlas-create-project"; protected description = "Create a MongoDB Atlas project"; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected argsShape = { projectName: AtlasArgs.projectName().optional().describe("Name for the new project"), organizationId: AtlasArgs.organizationId().optional().describe("Organization ID for the new project"), diff --git a/src/tools/atlas/read/getPerformanceAdvisor.ts b/src/tools/atlas/read/getPerformanceAdvisor.ts index f6245010..1f18470b 100644 --- a/src/tools/atlas/read/getPerformanceAdvisor.ts +++ b/src/tools/atlas/read/getPerformanceAdvisor.ts @@ -26,7 +26,7 @@ const PerformanceAdvisorOperationType = z.enum([ export class GetPerformanceAdvisorTool extends AtlasToolBase { public name = "atlas-get-performance-advisor"; protected description = `Get MongoDB Atlas performance advisor recommendations, which includes the operations: suggested indexes, drop index suggestions, schema suggestions, and a sample of the most recent (max ${DEFAULT_SLOW_QUERY_LOGS_LIMIT}) slow query logs`; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { projectId: AtlasArgs.projectId().describe( "Atlas project ID to get performance advisor recommendations. The project ID is a hexadecimal identifier of 24 characters. If the user has only specified the name, use the `atlas-list-projects` tool to retrieve the user's projects with their ids." diff --git a/src/tools/atlas/read/inspectAccessList.ts b/src/tools/atlas/read/inspectAccessList.ts index 7db73e7b..ed56206a 100644 --- a/src/tools/atlas/read/inspectAccessList.ts +++ b/src/tools/atlas/read/inspectAccessList.ts @@ -10,7 +10,7 @@ export const InspectAccessListArgs = { export class InspectAccessListTool extends AtlasToolBase { public name = "atlas-inspect-access-list"; protected description = "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters."; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { ...InspectAccessListArgs, }; diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index fd880610..d437db7d 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -13,7 +13,7 @@ export const InspectClusterArgs = { export class InspectClusterTool extends AtlasToolBase { public name = "atlas-inspect-cluster"; protected description = "Inspect MongoDB Atlas cluster"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { ...InspectClusterArgs, }; diff --git a/src/tools/atlas/read/listAlerts.ts b/src/tools/atlas/read/listAlerts.ts index 1e3a6998..7789fcda 100644 --- a/src/tools/atlas/read/listAlerts.ts +++ b/src/tools/atlas/read/listAlerts.ts @@ -10,7 +10,7 @@ export const ListAlertsArgs = { export class ListAlertsTool extends AtlasToolBase { public name = "atlas-list-alerts"; protected description = "List MongoDB Atlas alerts"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { ...ListAlertsArgs, }; diff --git a/src/tools/atlas/read/listClusters.ts b/src/tools/atlas/read/listClusters.ts index 1d1e3656..ef2cd344 100644 --- a/src/tools/atlas/read/listClusters.ts +++ b/src/tools/atlas/read/listClusters.ts @@ -18,7 +18,7 @@ export const ListClustersArgs = { export class ListClustersTool extends AtlasToolBase { public name = "atlas-list-clusters"; protected description = "List MongoDB Atlas clusters"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { ...ListClustersArgs, }; diff --git a/src/tools/atlas/read/listDBUsers.ts b/src/tools/atlas/read/listDBUsers.ts index 7103f266..50054472 100644 --- a/src/tools/atlas/read/listDBUsers.ts +++ b/src/tools/atlas/read/listDBUsers.ts @@ -11,7 +11,7 @@ export const ListDBUsersArgs = { export class ListDBUsersTool extends AtlasToolBase { public name = "atlas-list-db-users"; protected description = "List MongoDB Atlas database users"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { ...ListDBUsersArgs, }; diff --git a/src/tools/atlas/read/listOrgs.ts b/src/tools/atlas/read/listOrgs.ts index f8bb3200..112e9df3 100644 --- a/src/tools/atlas/read/listOrgs.ts +++ b/src/tools/atlas/read/listOrgs.ts @@ -6,7 +6,7 @@ import { formatUntrustedData } from "../../tool.js"; export class ListOrganizationsTool extends AtlasToolBase { public name = "atlas-list-orgs"; protected description = "List MongoDB Atlas organizations"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = {}; protected async execute(): Promise { diff --git a/src/tools/atlas/read/listProjects.ts b/src/tools/atlas/read/listProjects.ts index 5ee2ae8d..a58da6ea 100644 --- a/src/tools/atlas/read/listProjects.ts +++ b/src/tools/atlas/read/listProjects.ts @@ -8,7 +8,7 @@ import { AtlasArgs } from "../../args.js"; export class ListProjectsTool extends AtlasToolBase { public name = "atlas-list-projects"; protected description = "List MongoDB Atlas projects"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = { orgId: AtlasArgs.organizationId() .describe("Atlas organization ID to filter projects. If not provided, projects for all orgs are returned.") diff --git a/src/tools/atlas/tools.ts b/src/tools/atlas/tools.ts index c2822ec5..0b46621f 100644 --- a/src/tools/atlas/tools.ts +++ b/src/tools/atlas/tools.ts @@ -1,29 +1,13 @@ -import { ListClustersTool } from "./read/listClusters.js"; -import { ListProjectsTool } from "./read/listProjects.js"; -import { InspectClusterTool } from "./read/inspectCluster.js"; -import { CreateFreeClusterTool } from "./create/createFreeCluster.js"; -import { CreateAccessListTool } from "./create/createAccessList.js"; -import { InspectAccessListTool } from "./read/inspectAccessList.js"; -import { ListDBUsersTool } from "./read/listDBUsers.js"; -import { CreateDBUserTool } from "./create/createDBUser.js"; -import { CreateProjectTool } from "./create/createProject.js"; -import { ListOrganizationsTool } from "./read/listOrgs.js"; -import { ConnectClusterTool } from "./connect/connectCluster.js"; -import { ListAlertsTool } from "./read/listAlerts.js"; -import { GetPerformanceAdvisorTool } from "./read/getPerformanceAdvisor.js"; - -export const AtlasTools = [ - ListClustersTool, - ListProjectsTool, - InspectClusterTool, - CreateFreeClusterTool, - CreateAccessListTool, - InspectAccessListTool, - ListDBUsersTool, - CreateDBUserTool, - CreateProjectTool, - ListOrganizationsTool, - ConnectClusterTool, - ListAlertsTool, - GetPerformanceAdvisorTool, -]; +export { ListClustersTool } from "./read/listClusters.js"; +export { ListProjectsTool } from "./read/listProjects.js"; +export { InspectClusterTool } from "./read/inspectCluster.js"; +export { CreateFreeClusterTool } from "./create/createFreeCluster.js"; +export { CreateAccessListTool } from "./create/createAccessList.js"; +export { InspectAccessListTool } from "./read/inspectAccessList.js"; +export { ListDBUsersTool } from "./read/listDBUsers.js"; +export { CreateDBUserTool } from "./create/createDBUser.js"; +export { CreateProjectTool } from "./create/createProject.js"; +export { ListOrganizationsTool } from "./read/listOrgs.js"; +export { ConnectClusterTool } from "./connect/connectCluster.js"; +export { ListAlertsTool } from "./read/listAlerts.js"; +export { GetPerformanceAdvisorTool } from "./read/getPerformanceAdvisor.js"; diff --git a/src/tools/atlasLocal/connect/connectDeployment.ts b/src/tools/atlasLocal/connect/connectDeployment.ts index 7bf8db4b..e7b75de2 100644 --- a/src/tools/atlasLocal/connect/connectDeployment.ts +++ b/src/tools/atlasLocal/connect/connectDeployment.ts @@ -8,7 +8,7 @@ import type { ConnectionMetadata } from "../../../telemetry/types.js"; export class ConnectDeploymentTool extends AtlasLocalToolBase { public name = "atlas-local-connect-deployment"; protected description = "Connect to a MongoDB Atlas Local deployment"; - public operationType: OperationType = "connect"; + static operationType: OperationType = "connect"; protected argsShape = { deploymentName: CommonArgs.string().describe("Name of the deployment to connect to"), }; diff --git a/src/tools/atlasLocal/create/createDeployment.ts b/src/tools/atlasLocal/create/createDeployment.ts index 54f28e8a..08e765fa 100644 --- a/src/tools/atlasLocal/create/createDeployment.ts +++ b/src/tools/atlasLocal/create/createDeployment.ts @@ -7,7 +7,7 @@ import { CommonArgs } from "../../args.js"; export class CreateDeploymentTool extends AtlasLocalToolBase { public name = "atlas-local-create-deployment"; protected description = "Create a MongoDB Atlas local deployment"; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected argsShape = { deploymentName: CommonArgs.string().describe("Name of the deployment to create").optional(), }; diff --git a/src/tools/atlasLocal/delete/deleteDeployment.ts b/src/tools/atlasLocal/delete/deleteDeployment.ts index 669a1ab0..b29a323b 100644 --- a/src/tools/atlasLocal/delete/deleteDeployment.ts +++ b/src/tools/atlasLocal/delete/deleteDeployment.ts @@ -7,7 +7,7 @@ import { CommonArgs } from "../../args.js"; export class DeleteDeploymentTool extends AtlasLocalToolBase { public name = "atlas-local-delete-deployment"; protected description = "Delete a MongoDB Atlas local deployment"; - public operationType: OperationType = "delete"; + static operationType: OperationType = "delete"; protected argsShape = { deploymentName: CommonArgs.string().describe("Name of the deployment to delete"), }; diff --git a/src/tools/atlasLocal/read/listDeployments.ts b/src/tools/atlasLocal/read/listDeployments.ts index 32a54117..393a65fe 100644 --- a/src/tools/atlasLocal/read/listDeployments.ts +++ b/src/tools/atlasLocal/read/listDeployments.ts @@ -8,7 +8,7 @@ import type { Client } from "@mongodb-js/atlas-local"; export class ListDeploymentsTool extends AtlasLocalToolBase { public name = "atlas-local-list-deployments"; protected description = "List MongoDB Atlas local deployments"; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected argsShape = {}; protected async executeWithAtlasLocalClient(client: Client): Promise { diff --git a/src/tools/atlasLocal/tools.ts b/src/tools/atlasLocal/tools.ts index 451362ce..3148e351 100644 --- a/src/tools/atlasLocal/tools.ts +++ b/src/tools/atlasLocal/tools.ts @@ -1,6 +1,4 @@ -import { DeleteDeploymentTool } from "./delete/deleteDeployment.js"; -import { ListDeploymentsTool } from "./read/listDeployments.js"; -import { CreateDeploymentTool } from "./create/createDeployment.js"; -import { ConnectDeploymentTool } from "./connect/connectDeployment.js"; - -export const AtlasLocalTools = [ListDeploymentsTool, DeleteDeploymentTool, CreateDeploymentTool, ConnectDeploymentTool]; +export { DeleteDeploymentTool } from "./delete/deleteDeployment.js"; +export { ListDeploymentsTool } from "./read/listDeployments.js"; +export { CreateDeploymentTool } from "./create/createDeployment.js"; +export { ConnectDeploymentTool } from "./connect/connectDeployment.js"; diff --git a/src/tools/index.ts b/src/tools/index.ts index ded82006..80941089 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,13 +1,20 @@ -import { AtlasTools } from "./atlas/tools.js"; -import { AtlasLocalTools } from "./atlasLocal/tools.js"; -import { MongoDbTools } from "./mongodb/tools.js"; +import * as AtlasTools from "./atlas/tools.js"; +import * as AtlasLocalTools from "./atlasLocal/tools.js"; +import * as MongoDbTools from "./mongodb/tools.js"; -const AllTools = [...MongoDbTools, ...AtlasTools, ...AtlasLocalTools]; +const AllTools = { + ...MongoDbTools, + ...AtlasTools, + ...AtlasLocalTools, +} as const; +// Export all the different categories of tools export { AllTools, MongoDbTools, AtlasTools, AtlasLocalTools }; +// Export the base tool class and supporting types. export { ToolBase, + type ToolClass, type ToolConstructorParams, type ToolCategory, type OperationType, diff --git a/src/tools/mongodb/connect/connect.ts b/src/tools/mongodb/connect/connect.ts index fd5dc0ca..9a9343f0 100644 --- a/src/tools/mongodb/connect/connect.ts +++ b/src/tools/mongodb/connect/connect.ts @@ -14,15 +14,15 @@ export class ConnectTool extends MongoDBToolBase { connectionString: z.string().describe("MongoDB connection string (in the mongodb:// or mongodb+srv:// format)"), }; - public override operationType: OperationType = "connect"; + static operationType: OperationType = "connect"; - constructor({ session, config, telemetry, elicitation }: ToolConstructorParams) { - super({ session, config, telemetry, elicitation }); - session.on("connect", () => { + constructor(params: ToolConstructorParams) { + super(params); + params.session.on("connect", () => { this.disable(); }); - session.on("disconnect", () => { + params.session.on("disconnect", () => { this.enable(); }); } diff --git a/src/tools/mongodb/connect/switchConnection.ts b/src/tools/mongodb/connect/switchConnection.ts index 53ce500c..fff33a27 100644 --- a/src/tools/mongodb/connect/switchConnection.ts +++ b/src/tools/mongodb/connect/switchConnection.ts @@ -19,15 +19,15 @@ export class SwitchConnectionTool extends MongoDBToolBase { ), }; - public override operationType: OperationType = "connect"; + static operationType: OperationType = "connect"; - constructor({ session, config, telemetry, elicitation }: ToolConstructorParams) { - super({ session, config, telemetry, elicitation }); - session.on("connect", () => { + constructor(params: ToolConstructorParams) { + super(params); + params.session.on("connect", () => { this.enable(); }); - session.on("disconnect", () => { + params.session.on("disconnect", () => { this.disable(); }); } diff --git a/src/tools/mongodb/create/createCollection.ts b/src/tools/mongodb/create/createCollection.ts index 22f9336f..3806f1e2 100644 --- a/src/tools/mongodb/create/createCollection.ts +++ b/src/tools/mongodb/create/createCollection.ts @@ -8,7 +8,7 @@ export class CreateCollectionTool extends MongoDBToolBase { "Creates a new collection in a database. If the database doesn't exist, it will be created automatically."; protected argsShape = DbOperationArgs; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected async execute({ collection, database }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index d0aca246..ef553643 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -84,7 +84,7 @@ export class CreateIndexTool extends MongoDBToolBase { ), }; - public operationType: OperationType = "create"; + static operationType: OperationType = "create"; protected async execute({ database, diff --git a/src/tools/mongodb/create/insertMany.ts b/src/tools/mongodb/create/insertMany.ts index 61c24303..e68e97a1 100644 --- a/src/tools/mongodb/create/insertMany.ts +++ b/src/tools/mongodb/create/insertMany.ts @@ -15,27 +15,29 @@ const zSupportedEmbeddingParametersWithInput = zSupportedEmbeddingParameters.ext ), }); +const commonArgs = { + ...DbOperationArgs, + documents: z + .array(zEJSON().describe("An individual MongoDB document")) + .describe( + "The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()." + ), +} as const; + export class InsertManyTool extends MongoDBToolBase { public name = "insert-many"; protected description = "Insert an array of documents into a MongoDB collection"; - protected argsShape = { - ...DbOperationArgs, - documents: z - .array(zEJSON().describe("An individual MongoDB document")) - .describe( - "The array of documents to insert, matching the syntax of the document argument of db.collection.insertMany()." - ), - ...(this.isFeatureEnabled("search") - ? { - embeddingParameters: zSupportedEmbeddingParametersWithInput - .optional() - .describe( - "The embedding model and its parameters to use to generate embeddings for fields with vector search indexes. Note to LLM: If unsure which embedding model to use, ask the user before providing one." - ), - } - : {}), - }; - public operationType: OperationType = "create"; + protected argsShape = this.isFeatureEnabled("search") + ? { + ...commonArgs, + embeddingParameters: zSupportedEmbeddingParametersWithInput + .optional() + .describe( + "The embedding model and its parameters to use to generate embeddings for fields with vector search indexes. Note to LLM: If unsure which embedding model to use, ask the user before providing one." + ), + } + : commonArgs; + static operationType: OperationType = "create"; protected async execute({ database, diff --git a/src/tools/mongodb/delete/deleteMany.ts b/src/tools/mongodb/delete/deleteMany.ts index 835cbb4a..124250de 100644 --- a/src/tools/mongodb/delete/deleteMany.ts +++ b/src/tools/mongodb/delete/deleteMany.ts @@ -16,7 +16,7 @@ export class DeleteManyTool extends MongoDBToolBase { "The query filter, specifying the deletion criteria. Matches the syntax of the filter argument of db.collection.deleteMany()" ), }; - public operationType: OperationType = "delete"; + static operationType: OperationType = "delete"; protected async execute({ database, diff --git a/src/tools/mongodb/delete/dropCollection.ts b/src/tools/mongodb/delete/dropCollection.ts index 50bd008a..480d7cd5 100644 --- a/src/tools/mongodb/delete/dropCollection.ts +++ b/src/tools/mongodb/delete/dropCollection.ts @@ -9,7 +9,7 @@ export class DropCollectionTool extends MongoDBToolBase { protected argsShape = { ...DbOperationArgs, }; - public operationType: OperationType = "delete"; + static operationType: OperationType = "delete"; protected async execute({ database, collection }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/delete/dropDatabase.ts b/src/tools/mongodb/delete/dropDatabase.ts index d33682ce..ab7ec3c7 100644 --- a/src/tools/mongodb/delete/dropDatabase.ts +++ b/src/tools/mongodb/delete/dropDatabase.ts @@ -8,7 +8,7 @@ export class DropDatabaseTool extends MongoDBToolBase { protected argsShape = { database: DbOperationArgs.database, }; - public operationType: OperationType = "delete"; + static operationType: OperationType = "delete"; protected async execute({ database }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/delete/dropIndex.ts b/src/tools/mongodb/delete/dropIndex.ts index 7c62dd0b..a3af029d 100644 --- a/src/tools/mongodb/delete/dropIndex.ts +++ b/src/tools/mongodb/delete/dropIndex.ts @@ -21,7 +21,7 @@ export class DropIndexTool extends MongoDBToolBase { .default("classic") .describe("The type of index to be deleted. Is always set to 'classic'."), }; - public operationType: OperationType = "delete"; + static operationType: OperationType = "delete"; protected async execute(toolArgs: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/metadata/collectionIndexes.ts b/src/tools/mongodb/metadata/collectionIndexes.ts index 7f2df71e..55474ae7 100644 --- a/src/tools/mongodb/metadata/collectionIndexes.ts +++ b/src/tools/mongodb/metadata/collectionIndexes.ts @@ -20,7 +20,7 @@ export class CollectionIndexesTool extends MongoDBToolBase { public name = "collection-indexes"; protected description = "Describe the indexes for a collection"; protected argsShape = DbOperationArgs; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute({ database, collection }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/metadata/collectionSchema.ts b/src/tools/mongodb/metadata/collectionSchema.ts index ad74e9e7..5ee3c789 100644 --- a/src/tools/mongodb/metadata/collectionSchema.ts +++ b/src/tools/mongodb/metadata/collectionSchema.ts @@ -25,7 +25,7 @@ export class CollectionSchemaTool extends MongoDBToolBase { ), }; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute( { database, collection, sampleSize, responseBytesLimit }: ToolArgs, diff --git a/src/tools/mongodb/metadata/collectionStorageSize.ts b/src/tools/mongodb/metadata/collectionStorageSize.ts index c38ccc07..c4e9a57c 100644 --- a/src/tools/mongodb/metadata/collectionStorageSize.ts +++ b/src/tools/mongodb/metadata/collectionStorageSize.ts @@ -7,7 +7,7 @@ export class CollectionStorageSizeTool extends MongoDBToolBase { protected description = "Gets the size of the collection"; protected argsShape = DbOperationArgs; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute({ database, collection }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/metadata/dbStats.ts b/src/tools/mongodb/metadata/dbStats.ts index 830df410..4a99bb53 100644 --- a/src/tools/mongodb/metadata/dbStats.ts +++ b/src/tools/mongodb/metadata/dbStats.ts @@ -11,7 +11,7 @@ export class DbStatsTool extends MongoDBToolBase { database: DbOperationArgs.database, }; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute({ database }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/metadata/explain.ts b/src/tools/mongodb/metadata/explain.ts index 89cb5f35..5d5e7095 100644 --- a/src/tools/mongodb/metadata/explain.ts +++ b/src/tools/mongodb/metadata/explain.ts @@ -42,7 +42,7 @@ export class ExplainTool extends MongoDBToolBase { ), }; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute({ database, diff --git a/src/tools/mongodb/metadata/listCollections.ts b/src/tools/mongodb/metadata/listCollections.ts index fb879cad..69aca486 100644 --- a/src/tools/mongodb/metadata/listCollections.ts +++ b/src/tools/mongodb/metadata/listCollections.ts @@ -10,7 +10,7 @@ export class ListCollectionsTool extends MongoDBToolBase { database: DbOperationArgs.database, }; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute({ database }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/metadata/listDatabases.ts b/src/tools/mongodb/metadata/listDatabases.ts index e89b2549..8cdb4aab 100644 --- a/src/tools/mongodb/metadata/listDatabases.ts +++ b/src/tools/mongodb/metadata/listDatabases.ts @@ -8,7 +8,7 @@ export class ListDatabasesTool extends MongoDBToolBase { public name = "list-databases"; protected description = "List all databases for a MongoDB connection"; protected argsShape = {}; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute(): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/metadata/logs.ts b/src/tools/mongodb/metadata/logs.ts index b19fa72c..b027fdad 100644 --- a/src/tools/mongodb/metadata/logs.ts +++ b/src/tools/mongodb/metadata/logs.ts @@ -24,7 +24,7 @@ export class LogsTool extends MongoDBToolBase { .describe("The maximum number of log entries to return."), }; - public operationType: OperationType = "metadata"; + static operationType: OperationType = "metadata"; protected async execute({ type, limit }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index ad4eb33a..15e21bf6 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -54,7 +54,7 @@ export class AggregateTool extends MongoDBToolBase { ...DbOperationArgs, ...getAggregateArgs(this.isFeatureEnabled("search")), }; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected async execute( { database, collection, pipeline, responseBytesLimit }: ToolArgs, diff --git a/src/tools/mongodb/read/count.ts b/src/tools/mongodb/read/count.ts index 435c2c77..b2bb6d0d 100644 --- a/src/tools/mongodb/read/count.ts +++ b/src/tools/mongodb/read/count.ts @@ -21,7 +21,7 @@ export class CountTool extends MongoDBToolBase { ...CountArgs, }; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected async execute({ database, collection, query }: ToolArgs): Promise { const provider = await this.ensureConnected(); diff --git a/src/tools/mongodb/read/export.ts b/src/tools/mongodb/read/export.ts index 6f24aa88..65439ca6 100644 --- a/src/tools/mongodb/read/export.ts +++ b/src/tools/mongodb/read/export.ts @@ -49,7 +49,7 @@ export class ExportTool extends MongoDBToolBase { ].join("\n") ), }; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected async execute({ database, diff --git a/src/tools/mongodb/read/find.ts b/src/tools/mongodb/read/find.ts index eb006f33..79fa1c86 100644 --- a/src/tools/mongodb/read/find.ts +++ b/src/tools/mongodb/read/find.ts @@ -42,7 +42,7 @@ export class FindTool extends MongoDBToolBase { ...DbOperationArgs, ...FindArgs, }; - public operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected async execute( { database, collection, filter, projection, limit, sort, responseBytesLimit }: ToolArgs, diff --git a/src/tools/mongodb/tools.ts b/src/tools/mongodb/tools.ts index ffbb71d8..6cf13c31 100644 --- a/src/tools/mongodb/tools.ts +++ b/src/tools/mongodb/tools.ts @@ -1,49 +1,23 @@ -import { ConnectTool } from "./connect/connect.js"; -import { ListCollectionsTool } from "./metadata/listCollections.js"; -import { CollectionIndexesTool } from "./metadata/collectionIndexes.js"; -import { ListDatabasesTool } from "./metadata/listDatabases.js"; -import { CreateIndexTool } from "./create/createIndex.js"; -import { CollectionSchemaTool } from "./metadata/collectionSchema.js"; -import { FindTool } from "./read/find.js"; -import { InsertManyTool } from "./create/insertMany.js"; -import { DeleteManyTool } from "./delete/deleteMany.js"; -import { CollectionStorageSizeTool } from "./metadata/collectionStorageSize.js"; -import { CountTool } from "./read/count.js"; -import { DbStatsTool } from "./metadata/dbStats.js"; -import { AggregateTool } from "./read/aggregate.js"; -import { UpdateManyTool } from "./update/updateMany.js"; -import { RenameCollectionTool } from "./update/renameCollection.js"; -import { DropDatabaseTool } from "./delete/dropDatabase.js"; -import { DropCollectionTool } from "./delete/dropCollection.js"; -import { ExplainTool } from "./metadata/explain.js"; -import { CreateCollectionTool } from "./create/createCollection.js"; -import { LogsTool } from "./metadata/logs.js"; -import { ExportTool } from "./read/export.js"; -import { DropIndexTool } from "./delete/dropIndex.js"; -import { SwitchConnectionTool } from "./connect/switchConnection.js"; - -export const MongoDbTools = [ - ConnectTool, - SwitchConnectionTool, - ListCollectionsTool, - ListDatabasesTool, - CollectionIndexesTool, - DropIndexTool, - CreateIndexTool, - CollectionSchemaTool, - FindTool, - InsertManyTool, - DeleteManyTool, - CollectionStorageSizeTool, - CountTool, - DbStatsTool, - AggregateTool, - UpdateManyTool, - RenameCollectionTool, - DropDatabaseTool, - DropCollectionTool, - ExplainTool, - CreateCollectionTool, - LogsTool, - ExportTool, -]; +export { ConnectTool } from "./connect/connect.js"; +export { ListCollectionsTool } from "./metadata/listCollections.js"; +export { CollectionIndexesTool } from "./metadata/collectionIndexes.js"; +export { ListDatabasesTool } from "./metadata/listDatabases.js"; +export { CreateIndexTool } from "./create/createIndex.js"; +export { CollectionSchemaTool } from "./metadata/collectionSchema.js"; +export { FindTool } from "./read/find.js"; +export { InsertManyTool } from "./create/insertMany.js"; +export { DeleteManyTool } from "./delete/deleteMany.js"; +export { CollectionStorageSizeTool } from "./metadata/collectionStorageSize.js"; +export { CountTool } from "./read/count.js"; +export { DbStatsTool } from "./metadata/dbStats.js"; +export { AggregateTool } from "./read/aggregate.js"; +export { UpdateManyTool } from "./update/updateMany.js"; +export { RenameCollectionTool } from "./update/renameCollection.js"; +export { DropDatabaseTool } from "./delete/dropDatabase.js"; +export { DropCollectionTool } from "./delete/dropCollection.js"; +export { ExplainTool } from "./metadata/explain.js"; +export { CreateCollectionTool } from "./create/createCollection.js"; +export { LogsTool } from "./metadata/logs.js"; +export { ExportTool } from "./read/export.js"; +export { DropIndexTool } from "./delete/dropIndex.js"; +export { SwitchConnectionTool } from "./connect/switchConnection.js"; diff --git a/src/tools/mongodb/update/renameCollection.ts b/src/tools/mongodb/update/renameCollection.ts index 4992a322..e4cdee37 100644 --- a/src/tools/mongodb/update/renameCollection.ts +++ b/src/tools/mongodb/update/renameCollection.ts @@ -11,7 +11,7 @@ export class RenameCollectionTool extends MongoDBToolBase { newName: z.string().describe("The new name for the collection"), dropTarget: z.boolean().optional().default(false).describe("If true, drops the target collection if it exists"), }; - public operationType: OperationType = "update"; + static operationType: OperationType = "update"; protected async execute({ database, diff --git a/src/tools/mongodb/update/updateMany.ts b/src/tools/mongodb/update/updateMany.ts index 9d936757..5b0b0dd2 100644 --- a/src/tools/mongodb/update/updateMany.ts +++ b/src/tools/mongodb/update/updateMany.ts @@ -23,7 +23,7 @@ export class UpdateManyTool extends MongoDBToolBase { .optional() .describe("Controls whether to insert a new document if no documents match the filter"), }; - public operationType: OperationType = "update"; + static operationType: OperationType = "update"; protected async execute({ database, diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 8173b8d2..a1e16432 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -36,25 +36,232 @@ export type OperationType = "metadata" | "read" | "create" | "delete" | "update" * - `mongodb` is used for tools that interact with a MongoDB instance, such as finding documents, * aggregating data, listing databases/collections/indexes, creating indexes, etc. * - `atlas` is used for tools that interact with MongoDB Atlas, such as listing clusters, creating clusters, etc. + * - `atlas-local` is used for tools that interact with local Atlas deployments. */ export type ToolCategory = "mongodb" | "atlas" | "atlas-local"; +/** + * Parameters passed to the constructor of all tools that extends `ToolBase`. + * + * The MongoDB MCP Server automatically injects these parameters when + * constructing tools and registering to the MCP Server. + * + * See `Server.registerTools` method in `src/server.ts` for further reference. + */ export type ToolConstructorParams = { + /** + * The type of operation the tool performs (injected from the static + * `operationType` property on the Tool class). + */ + operationType: OperationType; + + /** + * An instance of Session class providing access to MongoDB connections, + * loggers, etc. + * + * See `src/common/session.ts` for further reference. + */ session: Session; + + /** + * The configuration object that MCP session was started with. + * + * See `src/common/config/userConfig.ts` for further reference. + */ config: UserConfig; + + /** + * The telemetry service for tracking tool usage. + * + * See `src/telemetry/telemetry.ts` for further reference. + */ telemetry: Telemetry; + + /** + * The elicitation service for requesting user confirmation. + * + * See `src/elicitation.ts` for further reference. + */ elicitation: Elicitation; }; +/** + * The type that all tool classes must conform to when implementing custom tools + * for the MongoDB MCP Server. + * + * This type enforces that tool classes have a static property `operationType` + * which is injected during instantiation of tool classes. + * + * @example + * ```typescript + * import { StreamableHttpRunner, UserConfigSchema } from "mongodb-mcp-server" + * import { ToolBase, type ToolClass, type ToolCategory, type OperationType } from "mongodb-mcp-server/tools"; + * import { z } from "zod"; + * + * class MyCustomTool extends ToolBase { + * // Required static properties for ToolClass conformance + * static operationType: OperationType = "read"; + * + * // Required abstract properties + * override name = "my-custom-tool"; + * public override category: ToolCategory = "mongodb"; + * protected description = "My custom tool description"; + * protected argsShape = { + * query: z.string().describe("The query parameter"), + * }; + * + * // Required abstract method: implement the tool's logic + * protected async execute(args) { + * // Tool implementation + * return { + * content: [{ type: "text", text: "Result" }], + * }; + * } + * + * // Required abstract method: provide telemetry metadata + * protected resolveTelemetryMetadata() { + * return {}; // Return empty object if no custom telemetry needed + * } + * } + * + * const runner = new StreamableHttpRunner({ + * userConfig: UserConfigSchema.parse({}), + * // This will work only if the class correctly conforms to ToolClass type, which in our case it does. + * tools: [MyCustomTool], + * }); + * ``` + */ +export type ToolClass = { + /** Constructor signature for the tool class */ + new (params: ToolConstructorParams): ToolBase; + + /** The type of operation the tool performs */ + operationType: OperationType; +}; + +/** + * Abstract base class for implementing MCP tools in the MongoDB MCP Server. + * + * All tools (both internal and custom) must extend this class to ensure a + * consistent interface and proper integration with the server. + * + * ## Creating a Custom Tool + * + * To create a custom tool, you must: + * 1. Extend the `ToolBase` class + * 2. Define static property: `operationType` + * 3. Implement required abstract members: `name`, `category`, `description`, + * `argsShape`, `execute()`, `resolveTelemetryMetadata()` + * + * @example Basic Custom Tool + * ```typescript + * import { StreamableHttpRunner, UserConfigSchema } from "mongodb-mcp-server" + * import { ToolBase, type ToolClass, type ToolCategory, type OperationType } from "mongodb-mcp-server/tools"; + * import { z } from "zod"; + * + * class MyCustomTool extends ToolBase { + * // Required static property for ToolClass conformance + * static operationType: OperationType = "read"; + * + * // Required abstract properties + * override name = "my-custom-tool"; + * override category: ToolCategory = "mongodb"; + * protected description = "My custom tool description"; + * protected argsShape = { + * query: z.string().describe("The query parameter"), + * }; + * + * // Required abstract method: implement the tool's logic + * protected async execute(args) { + * // Tool implementation + * return { + * content: [{ type: "text", text: "Result" }], + * }; + * } + * + * // Required abstract method: provide telemetry metadata + * protected resolveTelemetryMetadata() { + * return {}; // Return empty object if no custom telemetry needed + * } + * } + * + * const runner = new StreamableHttpRunner({ + * userConfig: UserConfigSchema.parse({}), + * // This will work only if the class correctly conforms to ToolClass type, which in our case it does. + * tools: [MyCustomTool], + * }); + * ``` + * + * ## Protected Members Available to Subclasses + * + * - `session` - Access to MongoDB connection, logger, and other session + * resources + * - `config` - Server configuration (`UserConfig`) + * - `telemetry` - Telemetry service for tracking usage + * - `elicitation` - Service for requesting user confirmations + * + * ## Instance Properties Set by Constructor + * + * The following property is automatically set when the tool is instantiated + * by the server (derived from the static property): + * - `operationType` - The tool's operation type (from static `operationType`) + * + * ## Optional Overrideable Methods + * + * - `getConfirmationMessage()` - Customize the confirmation prompt for tools + * requiring user approval + * - `handleError()` - Customize error handling behavior + * + * @see {@link ToolClass} for the type that tool classes must conform to + * @see {@link ToolConstructorParams} for the parameters passed to the + * constructor + */ export abstract class ToolBase { + /** + * The unique name of this tool. + * + * Must be unique across all tools in the server. + */ public abstract name: string; + /** + * The category of this tool. + * + * @see {@link ToolCategory} for the available tool categories. + */ public abstract category: ToolCategory; - public abstract operationType: OperationType; + /** + * The type of operation this tool performs. + * + * Automatically set from the static `operationType` property during + * construction. + * + * @see {@link OperationType} for the available tool operations. + */ + public operationType: OperationType; + /** + * Human-readable description of what the tool does. + * + * This is shown to the MCP client and helps the LLM understand when to use + * this tool. + */ protected abstract description: string; + /** + * Zod schema defining the tool's arguments. + * + * Use an empty object `{}` if the tool takes no arguments. + * + * @example + * ```typescript + * protected argsShape = { + * query: z.string().describe("The search query"), + * limit: z.number().optional().describe("Maximum results to return"), + * }; + * ``` + */ protected abstract argsShape: ZodRawShape; private registeredTool: RegisteredTool | undefined; @@ -87,16 +294,68 @@ export abstract class ToolBase { return annotations; } + /** + * A function that is registered as the tool execution callback and is + * called with the expected arguments. + * + * This is the core implementation of your tool's functionality. It receives + * validated arguments (validated against `argsShape`) and must return a + * result conforming to the MCP protocol. + * + * @param args - The validated arguments passed to the tool + * @returns A promise resolving to the tool execution result + * + * @example + * ```typescript + * protected async execute(args: { query: string }): Promise { + * const results = await this.session.db.collection('items').find({ + * name: { $regex: args.query, $options: 'i' } + * }).toArray(); + * + * return { + * content: [{ + * type: "text", + * text: JSON.stringify(results), + * }], + * }; + * } + * ``` + */ protected abstract execute(...args: ToolCallbackArgs): Promise; - /** Get the confirmation message for the tool. Can be overridden to provide a more specific message. */ + /** + * Get the confirmation message shown to users when this tool requires + * explicit approval. + * + * Override this method to provide a more specific and helpful confirmation + * message based on the tool's arguments. + * + * @param args - The tool arguments + * @returns The confirmation message to display to the user + * + * @example + * ```typescript + * protected getConfirmationMessage(args: { database: string }): string { + * return `You are about to delete the database "${args.database}". This action cannot be undone. Proceed?`; + * } + * ``` + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getConfirmationMessage(...args: ToolCallbackArgs): string { return `You are about to execute the \`${this.name}\` tool which requires additional confirmation. Would you like to proceed?`; } - /** Check if the user has confirmed the tool execution, if required by the configuration. - * Always returns true if confirmation is not required. + /** + * Check if the user has confirmed the tool execution (if required by + * configuration). + * + * This method automatically checks if the tool name is in the + * `confirmationRequiredTools` configuration list and requests user + * confirmation via the elicitation service if needed. + * + * @param args - The tool arguments + * @returns A promise resolving to `true` if confirmed or confirmation not + * required, `false` otherwise */ public async verifyConfirmed(args: ToolCallbackArgs): Promise { if (!this.config.confirmationRequiredTools.includes(this.name)) { @@ -106,11 +365,33 @@ export abstract class ToolBase { return this.elicitation.requestConfirmation(this.getConfirmationMessage(...args)); } + /** + * Access to the session instance. Provides access to MongoDB connections, + * loggers, connection manager, and other session-level resources. + */ protected readonly session: Session; + + /** + * Access to the server configuration. Contains all user configuration + * settings including connection strings, feature flags, and operational + * limits. + */ protected readonly config: UserConfig; + + /** + * Access to the telemetry service. Use this to emit custom telemetry events + * if needed. + */ protected readonly telemetry: Telemetry; + + /** + * Access to the elicitation service. Use this to request user confirmations + * or inputs during tool execution. + */ protected readonly elicitation: Elicitation; - constructor({ session, config, telemetry, elicitation }: ToolConstructorParams) { + + constructor({ operationType, session, config, telemetry, elicitation }: ToolConstructorParams) { + this.operationType = operationType; this.session = session; this.config = config; this.telemetry = telemetry; @@ -238,6 +519,33 @@ export abstract class ToolBase { return true; } + /** + * Handle errors that occur during tool execution. + * + * Override this method to provide custom error handling logic. The default + * implementation returns a simple error message. + * + * @param error - The error that was thrown + * @param args - The arguments that were passed to the tool + * @returns A CallToolResult with error information + * + * @example + * ```typescript + * protected handleError(error: unknown, args: { query: string }): CallToolResult { + * if (error instanceof MongoError && error.code === 11000) { + * return { + * content: [{ + * type: "text", + * text: `Duplicate key error for query: ${args.query}`, + * }], + * isError: true, + * }; + * } + * // Fall back to default error handling + * return super.handleError(error, args); + * } + * ``` + */ // This method is intended to be overridden by subclasses to handle errors protected handleError( error: unknown, @@ -255,6 +563,30 @@ export abstract class ToolBase { }; } + /** + * Resolve telemetry metadata for this tool execution. + * + * This method is called after every tool execution to collect metadata for + * telemetry events. Return an object with custom properties you want to + * track, or an empty object if no custom telemetry is needed. + * + * @param result - The result of the tool execution + * @param args - The arguments and context passed to the tool + * @returns An object containing telemetry metadata + * + * @example + * ```typescript + * protected resolveTelemetryMetadata( + * result: CallToolResult, + * args: { query: string } + * ): TelemetryToolMetadata { + * return { + * query_length: args.query.length, + * result_count: result.isError ? 0 : JSON.parse(result.content[0].text).length, + * }; + * } + * ``` + */ protected abstract resolveTelemetryMetadata( result: CallToolResult, ...args: Parameters> diff --git a/src/transports/base.ts b/src/transports/base.ts index 787df80a..8440e13e 100644 --- a/src/transports/base.ts +++ b/src/transports/base.ts @@ -20,7 +20,7 @@ import type { AtlasLocalClientFactoryFn } from "../common/atlasLocal.js"; import { defaultCreateAtlasLocalClient } from "../common/atlasLocal.js"; import type { Client } from "@mongodb-js/atlas-local"; import { VectorSearchEmbeddingsManager } from "../common/search/vectorSearchEmbeddingsManager.js"; -import type { ToolBase, ToolConstructorParams } from "../tools/tool.js"; +import type { ToolClass } from "../tools/tool.js"; import { applyConfigOverrides } from "../common/config/configOverrides.js"; export type RequestContext = { @@ -28,22 +28,160 @@ export type RequestContext = { query?: Record; }; +/** + * A function to dynamically generate `UserConfig` object, potentially unique to + * each MCP client session. + * + * The function is passed a config context object containing: + * 1. `userConfig`: The base `UserConfig` object that MongoDB MCP Server was + * started with, either through parsed CLI arguments or a static + * configuration injected through `TransportRunnerConfig` + * 2. `request`: An optional, `RequestContext` object, available only when + * MongoDB MCP server is running over HTTP transport, that contains headers + * and query parameters received in MCP session initialization object. + * + * @see {@link UserConfig} to inspect the properties available on `userConfig` + * object. + * @see {@link RequestContext} to inspect the properties available on + * `requestContext` object. + */ type CreateSessionConfigFn = (context: { userConfig: UserConfig; request?: RequestContext; }) => Promise | UserConfig; +/** + * Configuration options for customizing how transport runners are initialized. + * This includes specifying the base user configuration, providing custom + * connection management, and other advanced options. + * + * You may want to customize this configuration if you need to: + * - Provide a custom user configuration for different environments or users. + * - Override the default connection management to MongoDB deployments. + * - Provide a specific list of tools to be registered with the MCP server. + * + * In most cases, just providing the `UserConfig` object is sufficient, but + * advanced use-cases (such as embedding the MCP server in another application + * or supporting custom authentication flows) may require customizing other + * `TransportRunnerConfig` options as well. + */ export type TransportRunnerConfig = { + /** + * Base user configuration for the server. + * + * Can be generated by parsing CLI arguments, environment variables or + * written manually while conforming the interface requirements of + * `UserConfig`. + * + * To parse CLI arguments and environment variables in order to generate a + * `UserConfig` object, you can use `createUserConfig` function, also + * exported as `parseCliArgumentsAsUserConfig` through MCP server library + * exports. + * + * Optionally, you can also use `UserConfigSchema` (available through MCP + * server library exports) to create a default configuration - + * `UserConfigSchema.parse({})`. + */ userConfig: UserConfig; + + /** + * An optional factory function to generates an instance of + * `ConnectionManager`. When not provided, MongoDB MCP Server uses an + * internal implementation to manage connection to MongoDB deployments. + * + * Customize this only if the use-case involves handling the MongoDB + * connections differently and outside of MongoDB MCP server. + */ createConnectionManager?: ConnectionManagerFactoryFn; + + /** + * An optional function to handle connection related errors. When not + * provided, MongoDB MCP Server uses an internal implementation to handle + * the errors raised by internal implementation of `ConnectionManager` + * class. + * + * Customize this only if you need to handle the Connection errors different + * from the internal implementation or if you have provided a different + * implementation of `ConnectionManager` that might raise errors unknown to + * default internal connection error handler. + */ connectionErrorHandler?: ConnectionErrorHandler; + + /** + * An optional factory function to create a client for working with Atlas + * local deployments. When not provided, MongoDB MCP Server uses an internal + * implementation to create the local Atlas client. + */ createAtlasLocalClient?: AtlasLocalClientFactoryFn; + + /** + * An optional list of loggers to be used in addition to the default logger + * implementations. When not provided, MongoDB MCP Server will not utilize + * any loggers other than the default that it works with. + * + * Customize this only if the default enabled loggers (disk/stderr/mcp) are + * not covering your use-case. + */ additionalLoggers?: LoggerBase[]; + + /** + * An optional key value pair of telemetry properties that are reported to + * the telemetry backend. Most, if not all, of the properties are captured + * automatically. + */ telemetryProperties?: Partial; - tools?: (new (params: ToolConstructorParams) => ToolBase)[]; + + /** + * An optional list of tools constructors to be registered to the MongoDB + * MCP Server. + * + * When not provided, MongoDB MCP Server will register all internal tools. + * When specified, **only** the tools in this list will be registered. + * + * This allows you to: + * - Register only custom tools (excluding all internal tools) + * - Register a subset of internal tools alongside custom tools + * - Register all internal tools plus custom tools + * + * To include internal tools, import them from `mongodb-mcp-server/tools`: + * + * ```typescript + * import { AllTools, MongoDbTools } from "mongodb-mcp-server/tools"; + * + * // Register all internal tools plus custom tools + * tools: [...Object.values(AllTools), MyCustomTool] + * + * // Register only MongoDB tools plus custom tools (exclude Atlas tools) + * tools: [...Object.values(MongoDbTools), MyCustomTool] + * ``` + * + * Note: Ensure that each tool has unique names otherwise the server will + * throw an error when initializing an MCP Client session. If you're using + * only the internal tools, then you don't have to worry about it unless, + * you've overridden the tool names. + * + * To ensure that you provide compliant tool implementations extend your + * tool implementation using `ToolBase` class and ensure that they conform + * to `ToolClass` type. + * + * @see {@link ToolClass} for the type that tool classes must conform to + * @see {@link ToolBase} for base class for all the tools + */ + tools?: ToolClass[]; + /** - * Hook which allows library consumers to fetch configuration from external sources (e.g., secrets managers, APIs) - * or modify the existing configuration before the session is created. + * An optional function to hook into session configuration lifecycle and + * provide session specific configuration (`UserConfig`). + * + * The function is called before each session is created, allowing you to: + * - Fetch configuration from external sources (secrets managers, APIs) + * - Apply user-specific permissions and limits + * - Modify connection strings dynamically + * - Validate authentication credentials + * + * This function is called for each new MCP client connection. For stdio + * transport, this is called once at server startup. For HTTP transport, + * this is called for each new session. */ createSessionConfig?: CreateSessionConfigFn; }; @@ -56,7 +194,7 @@ export abstract class TransportRunnerBase { private readonly connectionErrorHandler: ConnectionErrorHandler; private readonly atlasLocalClient: Promise; private readonly telemetryProperties: Partial; - private readonly tools?: (new (params: ToolConstructorParams) => ToolBase)[]; + private readonly tools?: ToolClass[]; private readonly createSessionConfig?: CreateSessionConfigFn; protected constructor({ diff --git a/tests/integration/customTools.test.ts b/tests/integration/customTools.test.ts index 955999d3..504fc298 100644 --- a/tests/integration/customTools.test.ts +++ b/tests/integration/customTools.test.ts @@ -81,7 +81,7 @@ describe("Custom Tools", () => { class CustomGreetingTool extends ToolBase { name = "custom_greeting"; category = "mongodb" as const; - operationType = "read" as const; + static operationType = "read" as const; protected description = "A custom tool that greets the user"; protected argsShape = { name: z.string().describe("The name to greet"), @@ -109,7 +109,7 @@ class CustomGreetingTool extends ToolBase { class CustomCalculatorTool extends ToolBase { name = "custom_calculator"; category = "mongodb" as const; - operationType = "read" as const; + static operationType = "read" as const; protected description = "A custom tool that performs calculations"; protected argsShape = { a: z.number().describe("First number"), diff --git a/tests/integration/tools/mongodb/mongodbTool.test.ts b/tests/integration/tools/mongodb/mongodbTool.test.ts index 12d37469..c5164692 100644 --- a/tests/integration/tools/mongodb/mongodbTool.test.ts +++ b/tests/integration/tools/mongodb/mongodbTool.test.ts @@ -3,7 +3,7 @@ import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { MongoDBToolBase } from "../../../../src/tools/mongodb/mongodbTool.js"; -import { type ToolBase, type ToolConstructorParams, type OperationType } from "../../../../src/tools/tool.js"; +import { type OperationType, type ToolClass } from "../../../../src/tools/tool.js"; import { type UserConfig } from "../../../../src/common/config/userConfig.js"; import { MCPConnectionManager } from "../../../../src/common/connectionManager.js"; import { Session } from "../../../../src/common/session.js"; @@ -19,7 +19,7 @@ import { setupMongoDBIntegrationTest } from "./mongodbHelpers.js"; import { ErrorCodes } from "../../../../src/common/errors.js"; import { Keychain } from "../../../../src/common/keychain.js"; import { Elicitation } from "../../../../src/elicitation.js"; -import { MongoDbTools } from "../../../../src/tools/mongodb/tools.js"; +import { MongoDbTools } from "../../../../src/tools/index.js"; import { VectorSearchEmbeddingsManager } from "../../../../src/common/search/vectorSearchEmbeddingsManager.js"; const injectedErrorHandler: ConnectionErrorHandler = (error) => { @@ -55,7 +55,7 @@ const injectedErrorHandler: ConnectionErrorHandler = (error) => { class RandomTool extends MongoDBToolBase { name = "Random"; - operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected description = "This is a tool."; protected argsShape = {}; public async execute(): Promise { @@ -66,7 +66,7 @@ class RandomTool extends MongoDBToolBase { class UnusableVoyageTool extends MongoDBToolBase { name = "UnusableVoyageTool"; - operationType: OperationType = "read"; + static operationType: OperationType = "read"; protected description = "This is a Voyage tool."; protected argsShape = {}; @@ -89,7 +89,7 @@ describe("MongoDBTool implementations", () => { async function cleanupAndStartServer( config: Partial | undefined = {}, - toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[] = [...MongoDbTools, RandomTool], + toolConstructors: ToolClass[] = [...Object.values(MongoDbTools), RandomTool], errorHandler: ConnectionErrorHandler | undefined = connectionErrorHandler ): Promise { await cleanup(); @@ -237,7 +237,11 @@ describe("MongoDBTool implementations", () => { describe("when MCP is using injected connection error handler", () => { beforeEach(async () => { - await cleanupAndStartServer(defaultTestConfig, [...MongoDbTools, RandomTool], injectedErrorHandler); + await cleanupAndStartServer( + defaultTestConfig, + [...Object.values(MongoDbTools), RandomTool], + injectedErrorHandler + ); }); describe("and comes across a MongoDB Error - NotConnectedToMongoDB", () => { @@ -263,7 +267,7 @@ describe("MongoDBTool implementations", () => { // This is a misconfigured connection string await cleanupAndStartServer( { connectionString: "mongodb://localhost:1234" }, - [...MongoDbTools, RandomTool], + [...Object.values(MongoDbTools), RandomTool], injectedErrorHandler ); const toolResponse = await mcpClient?.callTool({ @@ -287,7 +291,7 @@ describe("MongoDBTool implementations", () => { // This is a misconfigured connection string await cleanupAndStartServer( { connectionString: mdbIntegration.connectionString(), indexCheck: true }, - [...MongoDbTools, RandomTool], + [...Object.values(MongoDbTools), RandomTool], injectedErrorHandler ); const toolResponse = await mcpClient?.callTool({ diff --git a/tests/unit/toolBase.test.ts b/tests/unit/toolBase.test.ts index 002b0426..82747955 100644 --- a/tests/unit/toolBase.test.ts +++ b/tests/unit/toolBase.test.ts @@ -60,6 +60,7 @@ describe("ToolBase", () => { } as unknown as Elicitation; const constructorParams: ToolConstructorParams = { + operationType: TestTool.operationType, session: mockSession, config: mockConfig, telemetry: mockTelemetry, @@ -264,7 +265,7 @@ describe("ToolBase", () => { class TestTool extends ToolBase { public name = "test-tool"; public category: ToolCategory = "mongodb"; - public operationType: OperationType = "delete"; + static operationType: OperationType = "delete"; protected description = "A test tool for verification tests"; protected argsShape = { param1: z.string().describe("Test parameter 1"), From c5bffeb8625b6008656ed5df7bdfae3619360574 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 1 Dec 2025 15:10:16 +0100 Subject: [PATCH 3/7] chore: further refine the examples --- MCP_SERVER_LIBRARY.md | 57 +++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/MCP_SERVER_LIBRARY.md b/MCP_SERVER_LIBRARY.md index 8d5c5240..94794102 100644 --- a/MCP_SERVER_LIBRARY.md +++ b/MCP_SERVER_LIBRARY.md @@ -703,7 +703,9 @@ const runner = new StreamableHttpRunner({ }); await runner.start(); -console.log("MongoDB MCP Server running with minimal toolset"); +console.log( + `MongoDB MCP Server running with minimal toolset at ${runner.serverAddress}` +); ``` In this configuration: @@ -910,14 +912,16 @@ The default connection manager factory (`createMCPConnectionManager`) is also ex ```typescript import { + ConnectionManager, StreamableHttpRunner, + UserConfigSchema, createMCPConnectionManager, } from "mongodb-mcp-server"; import type { ConnectionManagerFactoryFn } from "mongodb-mcp-server"; // Using the default connection manager (this is the default behavior) const runner1 = new StreamableHttpRunner({ - userConfig: config, + userConfig: UserConfigSchema.parse({}), createConnectionManager: createMCPConnectionManager, }); @@ -926,13 +930,15 @@ const customConnectionManager: ConnectionManagerFactoryFn = async ({ logger, userConfig, deviceId, -}) => { - // Return a custom ConnectionManager implementation - // that could delegate to your application's existing connection logic +}): Promise => { + // Just for types we're using the internal mcp connection manager factory but + // its an example. You can return a custom ConnectionManager implementation + // that could delegate to your application's existing connection logic. + return createMCPConnectionManager({ logger, userConfig, deviceId }); }; const runner2 = new StreamableHttpRunner({ - userConfig: config, + userConfig: UserConfigSchema.parse({}), createConnectionManager: customConnectionManager, }); ``` @@ -984,9 +990,10 @@ const customErrorHandler: ConnectionErrorHandler = (error, context) => { console.error("Connection error:", error.code, error.message); // Access available tools and connection state - const connectTools = context.availableTools.filter( - (t) => t.operationType === "connect" - ); + const connectTools = context.availableTools + .filter((t) => t.operationType === "connect") + .map((tool) => tool.name) + .join(", "); if (error.code === ErrorCodes.NotConnectedToMongoDB) { // Provide custom error message @@ -996,7 +1003,7 @@ const customErrorHandler: ConnectionErrorHandler = (error, context) => { content: [ { type: "text", - text: "Please connect to MongoDB first using one of the available connect tools.", + text: `Please connect to MongoDB first using one of the available connect tools - (${connectTools})`, }, ], isError: true, @@ -1009,7 +1016,7 @@ const customErrorHandler: ConnectionErrorHandler = (error, context) => { }; const runner2 = new StreamableHttpRunner({ - userConfig: config, + userConfig: UserConfigSchema.parse({}), connectionErrorHandler: customErrorHandler, }); ``` @@ -1023,16 +1030,36 @@ import { StreamableHttpRunner, LoggerBase, UserConfigSchema, + Keychain, type LogPayload, + type LogLevel, + type LoggerType, } from "mongodb-mcp-server"; class CustomLogger extends LoggerBase { - log(payload: LogPayload): void { - // Send to your logging service - console.log(`[${payload.id}] ${payload.message}`); + // Optional: specify the logger type for redaction control + protected readonly type: LoggerType = "console"; + + constructor() { + // Pass keychain for automatic secret redaction + // Use Keychain.root for the global keychain or create your own + super(Keychain.root); } - // Implement other log level methods... + // Required: implement the core logging method + protected logCore(level: LogLevel, payload: LogPayload): void { + // Send to your logging service + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] [${level.toUpperCase()}] [${payload.id}] ${payload.context}: ${payload.message}`; + + // Example: Send to external logging service + console.log(logMessage); + + // You can also access payload.attributes for additional context + if (payload.attributes) { + console.log(" Attributes:", JSON.stringify(payload.attributes)); + } + } } const runner = new StreamableHttpRunner({ From 2ea82d5295cab3552d97c0a68b2350993b949f1b Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 1 Dec 2025 16:18:34 +0100 Subject: [PATCH 4/7] chore: fix knip config --- knip.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/knip.json b/knip.json index 760eeb89..d679972a 100644 --- a/knip.json +++ b/knip.json @@ -2,18 +2,12 @@ "entry": [ "src/index.ts!", "src/lib.ts!", + "src/tools/index.ts!", "tests/**/*.ts", "scripts/**/*.ts", "eslint-rules/*.js" ], - "ignore": [ - // Knip, for some reason, is not able to link the exported tools to the - // final exports in `tools/index.ts` and complains about unused exports. For - // that reason we're ignoring the tool definitions. - "src/tools/**/*.ts", - "tests/integration/fixtures/curl.mjs", - "tests/vitest.d.ts" - ], + "ignore": ["tests/integration/fixtures/curl.mjs", "tests/vitest.d.ts"], "ignoreDependencies": ["@mongodb-js/atlas-local"], "ignoreExportsUsedInFile": true } From e00143b0b5f32d7076a5c38ce2fcd226924accfb Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 1 Dec 2025 17:22:54 +0100 Subject: [PATCH 5/7] chore: move category to static property --- MCP_SERVER_LIBRARY.md | 97 +++++-------------- src/server.ts | 7 +- src/tools/atlas/atlasTool.ts | 2 +- src/tools/atlasLocal/atlasLocalTool.ts | 2 +- src/tools/index.ts | 12 ++- src/tools/mongodb/mongodbTool.ts | 2 +- src/tools/tool.ts | 31 ++++-- src/transports/base.ts | 11 ++- tests/integration/customTools.test.ts | 4 +- .../tools/mongodb/mongodbTool.test.ts | 2 +- tests/unit/toolBase.test.ts | 3 +- 11 files changed, 71 insertions(+), 102 deletions(-) diff --git a/MCP_SERVER_LIBRARY.md b/MCP_SERVER_LIBRARY.md index 94794102..dd0da22b 100644 --- a/MCP_SERVER_LIBRARY.md +++ b/MCP_SERVER_LIBRARY.md @@ -459,7 +459,7 @@ const AVAILABLE_CONNECTIONS = { // connected to. class ListConnectionsTool extends ToolBase { override name = "list-connections"; - override category: ToolCategory = "mongodb"; + static category: ToolCategory = "mongodb"; static operationType: OperationType = "metadata"; protected override description = "Lists all available pre-configured MongoDB connections"; @@ -501,7 +501,7 @@ class ListConnectionsTool extends ToolBase { // effective communication using opaque connection identifiers. class SelectConnectionTool extends ToolBase { override name = "select-connection"; - override category: ToolCategory = "mongodb"; + static category: ToolCategory = "mongodb"; static operationType: OperationType = "metadata"; protected override description = "Select and connect to a pre-configured MongoDB connection by ID"; @@ -583,9 +583,7 @@ const runner = new StdioRunner({ }), // Register all internal tools except the default connect tools, plus our custom tools tools: [ - ...Object.values(AllTools).filter( - (tool) => tool.operationType !== "connect" - ), + ...AllTools.filter((tool) => tool.operationType !== "connect"), ListConnectionsTool, SelectConnectionTool, ], @@ -619,7 +617,7 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; // Custom tool to fetch ticket details from your application class GetTicketDetailsTool extends ToolBase { override name = "get-ticket-details"; - override category: ToolCategory = "mongodb"; + static category: ToolCategory = "mongodb"; static operationType: OperationType = "read"; protected override description = @@ -741,7 +739,7 @@ See the TypeScript documentation in [`src/tools/tool.ts`](./src/tools/tool.ts) f **Important:** All custom tools must conform to the `ToolClass` type, which requires: -- A **static** `operationType` property (not an instance property) +- **Static** `category` and `operationType` properties (not instance properties) - Implementation of all abstract members from `ToolBase` ### ToolClass @@ -751,9 +749,9 @@ The type that all tool classes must conform to when implementing custom tools. This type enforces that tool classes have: - A constructor that accepts `ToolConstructorParams` -- A **static** `operationType` property +- **Static** `category` and `operationType` properties -The static `operationType` is automatically injected as an instance property during tool construction by the server. +The static properties are automatically injected as instance properties during tool construction by the server. See the TypeScript documentation in [`src/tools/tool.ts`](./src/tools/tool.ts) for complete details and examples. @@ -761,80 +759,31 @@ See the TypeScript documentation in [`src/tools/tool.ts`](./src/tools/tool.ts) f The library exports collections of internal tool classes that can be used for selective tool registration or extension. -#### AllTools - -An object containing all internal tool classes (MongoDB, Atlas, and Atlas Local tools combined). - ```typescript -import { AllTools, MongoDbTools, AtlasTools } from "mongodb-mcp-server/tools"; +import { AllTools, AggregateTool, FindTool } from "mongodb-mcp-server/tools"; -// Pick a specific tool -const MyTool = AllTools.AggregateTool; +// Use all internal tools +// An array containing all internal tool constructors (MongoDB, Atlas, and Atlas Local tools combined). +const allTools = AllTools; -// Create a list of hand picked tools -const selectedInternalTools = [ - AllTools.AggregateTool, - AllTools.ConnectTool, - AllTools.SwitchConnectionTool, -]; +// Pick specific tools by importing them directly +const selectedInternalTools = [AggregateTool, FindTool]; -// Create a list of all internal tools except a few -const filteredTools = Object.values(AllTools).filter( - (tool) => - tool !== AllTools.ConnectTool && tool !== AllTools.SwitchConnectionTool +// Create a list of all internal tools except a few by filtering +const filteredTools = AllTools.filter( + (tool) => tool !== AggregateTool && tool !== FindTool ); // Filter tools by operationType (static property) -const connectionRelatedTools = Object.values(AllTools).filter( +const connectionRelatedTools = AllTools.filter( (tool) => tool.operationType === "connect" ); -``` - -#### MongoDbTools - -An object containing only MongoDB-specific tool classes (tools that interact with MongoDB deployments). - -```typescript -import { MongoDbTools } from "mongodb-mcp-server/tools"; - -// Get all MongoDB tools as an array -const mongoTools = Object.values(MongoDbTools); - -// You can check static properties like operationType -const readOnlyMongoTools = mongoTools.filter( - (tool) => tool.operationType === "read" || tool.operationType === "metadata" -); -``` - -#### AtlasTools - -An object containing only MongoDB Atlas-specific tool classes (tools that interact with Atlas API). - -```typescript -import { AtlasTools } from "mongodb-mcp-server/tools"; - -// Get all Atlas tools as an array -const atlasTools = Object.values(AtlasTools); -// You can check static properties like operationType -const atlasCreationTools = atlasTools.filter( - (tool) => tool.operationType === "create" -); -``` - -#### AtlasLocalTools - -An object containing only Atlas Local-specific tool classes (tools that interact with local Atlas deployments). - -```typescript -import { AtlasLocalTools } from "mongodb-mcp-server/tools"; - -// Get all Atlas Local tools as an array -const atlasLocalTools = Object.values(AtlasLocalTools); - -// You can check static properties like operationType -const atlasLocalConnectionTools = atlasLocalTools.filter( - (tool) => tool.operationType === "connect" +// Filter tools by category +const mongodbTools = AllTools.filter((tool) => tool.category === "mongodb"); +const atlasTools = AllTools.filter((tool) => tool.category === "atlas"); +const atlasLocalTools = AllTools.filter( + (tool) => tool.category === "atlas-local" ); ``` @@ -1101,7 +1050,7 @@ For complete working examples of embedding and extending the MongoDB MCP Server, **Problem:** Custom tools not appearing in the tool list - **Solution:** Ensure the tool class extends `ToolBase` and is passed in the `tools` array -- **Solution:** If you want both internal and custom tools, spread `AllTools` in the array: `tools: [...Object.values(AllTools), MyCustomTool]` +- **Solution:** If you want both internal and custom tools, spread `AllTools` in the array: `tools: [...AllTools, MyCustomTool]` - **Solution:** Check that the tool's `verifyAllowed()` returns true and the tool is not accidentally disabled by config (disabledTools) **Problem:** Configuration overrides not working diff --git a/src/server.ts b/src/server.ts index 9fbe2ecb..daec7a0a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -38,7 +38,7 @@ export interface ServerOptions { * import { AllTools, ToolBase, type ToolCategory, type OperationType } from "mongodb-mcp-server/tools"; * class CustomTool extends ToolBase { * override name = "custom_tool"; - * override category: ToolCategory = "mongodb"; + * static category: ToolCategory = "mongodb"; * static operationType: OperationType = "read"; * protected description = "Custom tool description"; * protected argsShape = {}; @@ -56,7 +56,7 @@ export interface ServerOptions { * telemetry: myTelemetry, * elicitation: myElicitation, * connectionErrorHandler: myConnectionErrorHandler, - * tools: [...Object.values(AllTools), CustomTool], + * tools: [...AllTools, CustomTool], * }); * ``` */ @@ -98,7 +98,7 @@ export class Server { this.userConfig = userConfig; this.elicitation = elicitation; this.connectionErrorHandler = connectionErrorHandler; - this.toolConstructors = tools ?? Object.values(AllTools); + this.toolConstructors = tools ?? AllTools; } async connect(transport: Transport): Promise { @@ -251,6 +251,7 @@ export class Server { private registerTools(): void { for (const toolConstructor of this.toolConstructors) { const tool = new toolConstructor({ + category: toolConstructor.category, operationType: toolConstructor.operationType, session: this.session, config: this.userConfig, diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index f1d6ee4e..da7488d5 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { ApiClientError } from "../../common/atlas/apiClientError.js"; export abstract class AtlasToolBase extends ToolBase { - public category: ToolCategory = "atlas"; + static category: ToolCategory = "atlas"; protected verifyAllowed(): boolean { if (!this.config.apiClientId || !this.config.apiClientSecret) { diff --git a/src/tools/atlasLocal/atlasLocalTool.ts b/src/tools/atlasLocal/atlasLocalTool.ts index 67b66872..c7ef7b12 100644 --- a/src/tools/atlasLocal/atlasLocalTool.ts +++ b/src/tools/atlasLocal/atlasLocalTool.ts @@ -9,7 +9,7 @@ import type { ConnectionMetadata } from "../../telemetry/types.js"; export const AtlasLocalToolMetadataDeploymentIdKey = "deploymentId"; export abstract class AtlasLocalToolBase extends ToolBase { - public category: ToolCategory = "atlas-local"; + static category: ToolCategory = "atlas-local"; protected verifyAllowed(): boolean { return this.session.atlasLocalClient !== undefined && super.verifyAllowed(); diff --git a/src/tools/index.ts b/src/tools/index.ts index 80941089..5d3af369 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,15 +1,19 @@ import * as AtlasTools from "./atlas/tools.js"; import * as AtlasLocalTools from "./atlasLocal/tools.js"; import * as MongoDbTools from "./mongodb/tools.js"; +import type { ToolClass } from "./tool.js"; -const AllTools = { +// Export the collection of tools for easier reference +export const AllTools: ToolClass[] = Object.values({ ...MongoDbTools, ...AtlasTools, ...AtlasLocalTools, -} as const; +}); -// Export all the different categories of tools -export { AllTools, MongoDbTools, AtlasTools, AtlasLocalTools }; +// Export all the individual tools for handpicking +export * from "./atlas/tools.js"; +export * from "./atlasLocal/tools.js"; +export * from "./mongodb/tools.js"; // Export the base tool class and supporting types. export { diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 1d2969f3..e86de919 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -16,7 +16,7 @@ export const DbOperationArgs = { export abstract class MongoDBToolBase extends ToolBase { protected server?: Server; - public category: ToolCategory = "mongodb"; + static category: ToolCategory = "mongodb"; protected async ensureConnected(): Promise { if (!this.session.isConnectedToMongoDB) { diff --git a/src/tools/tool.ts b/src/tools/tool.ts index a1e16432..5c00cfab 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -49,6 +49,12 @@ export type ToolCategory = "mongodb" | "atlas" | "atlas-local"; * See `Server.registerTools` method in `src/server.ts` for further reference. */ export type ToolConstructorParams = { + /** + * The category that the tool belongs to (injected from the static + * `category` property on the Tool class). + */ + category: ToolCategory; + /** * The type of operation the tool performs (injected from the static * `operationType` property on the Tool class). @@ -89,8 +95,8 @@ export type ToolConstructorParams = { * The type that all tool classes must conform to when implementing custom tools * for the MongoDB MCP Server. * - * This type enforces that tool classes have a static property `operationType` - * which is injected during instantiation of tool classes. + * This type enforces that tool classes have static properties `category` and + * `operationType` which are injected during instantiation of tool classes. * * @example * ```typescript @@ -100,11 +106,11 @@ export type ToolConstructorParams = { * * class MyCustomTool extends ToolBase { * // Required static properties for ToolClass conformance + * static category: ToolCategory = "mongodb"; * static operationType: OperationType = "read"; * * // Required abstract properties * override name = "my-custom-tool"; - * public override category: ToolCategory = "mongodb"; * protected description = "My custom tool description"; * protected argsShape = { * query: z.string().describe("The query parameter"), @@ -135,6 +141,9 @@ export type ToolClass = { /** Constructor signature for the tool class */ new (params: ToolConstructorParams): ToolBase; + /** The category that the tool belongs to */ + category: ToolCategory; + /** The type of operation the tool performs */ operationType: OperationType; }; @@ -149,8 +158,8 @@ export type ToolClass = { * * To create a custom tool, you must: * 1. Extend the `ToolBase` class - * 2. Define static property: `operationType` - * 3. Implement required abstract members: `name`, `category`, `description`, + * 2. Define static properties: `category` and `operationType` + * 3. Implement required abstract members: `name`, `description`, * `argsShape`, `execute()`, `resolveTelemetryMetadata()` * * @example Basic Custom Tool @@ -161,11 +170,11 @@ export type ToolClass = { * * class MyCustomTool extends ToolBase { * // Required static property for ToolClass conformance + * static category: ToolCategory = "mongodb"; * static operationType: OperationType = "read"; * * // Required abstract properties * override name = "my-custom-tool"; - * override category: ToolCategory = "mongodb"; * protected description = "My custom tool description"; * protected argsShape = { * query: z.string().describe("The query parameter"), @@ -202,8 +211,9 @@ export type ToolClass = { * * ## Instance Properties Set by Constructor * - * The following property is automatically set when the tool is instantiated - * by the server (derived from the static property): + * The following properties are automatically set when the tool is instantiated + * by the server (derived from the static properties): + * - `category` - The tool's category (from static `category`) * - `operationType` - The tool's operation type (from static `operationType`) * * ## Optional Overrideable Methods @@ -229,7 +239,7 @@ export abstract class ToolBase { * * @see {@link ToolCategory} for the available tool categories. */ - public abstract category: ToolCategory; + public category: ToolCategory; /** * The type of operation this tool performs. @@ -390,7 +400,8 @@ export abstract class ToolBase { */ protected readonly elicitation: Elicitation; - constructor({ operationType, session, config, telemetry, elicitation }: ToolConstructorParams) { + constructor({ category, operationType, session, config, telemetry, elicitation }: ToolConstructorParams) { + this.category = category; this.operationType = operationType; this.session = session; this.config = config; diff --git a/src/transports/base.ts b/src/transports/base.ts index 8440e13e..3e6a661b 100644 --- a/src/transports/base.ts +++ b/src/transports/base.ts @@ -146,13 +146,16 @@ export type TransportRunnerConfig = { * To include internal tools, import them from `mongodb-mcp-server/tools`: * * ```typescript - * import { AllTools, MongoDbTools } from "mongodb-mcp-server/tools"; + * import { AllTools, AggregateTool, FindTool } from "mongodb-mcp-server/tools"; * * // Register all internal tools plus custom tools - * tools: [...Object.values(AllTools), MyCustomTool] + * tools: [...AllTools, MyCustomTool] * - * // Register only MongoDB tools plus custom tools (exclude Atlas tools) - * tools: [...Object.values(MongoDbTools), MyCustomTool] + * // Register only specific MongoDB tools plus custom tools + * tools: [AggregateTool, FindTool, MyCustomTool] + * + * // Register all internal tools of mongodb category + * tools: [AllTools.filter((tool) => tool.category === "mongodb")] * ``` * * Note: Ensure that each tool has unique names otherwise the server will diff --git a/tests/integration/customTools.test.ts b/tests/integration/customTools.test.ts index 504fc298..0954e9e4 100644 --- a/tests/integration/customTools.test.ts +++ b/tests/integration/customTools.test.ts @@ -80,7 +80,7 @@ describe("Custom Tools", () => { */ class CustomGreetingTool extends ToolBase { name = "custom_greeting"; - category = "mongodb" as const; + static category = "mongodb" as const; static operationType = "read" as const; protected description = "A custom tool that greets the user"; protected argsShape = { @@ -108,7 +108,7 @@ class CustomGreetingTool extends ToolBase { */ class CustomCalculatorTool extends ToolBase { name = "custom_calculator"; - category = "mongodb" as const; + static category = "mongodb" as const; static operationType = "read" as const; protected description = "A custom tool that performs calculations"; protected argsShape = { diff --git a/tests/integration/tools/mongodb/mongodbTool.test.ts b/tests/integration/tools/mongodb/mongodbTool.test.ts index c5164692..7312d29e 100644 --- a/tests/integration/tools/mongodb/mongodbTool.test.ts +++ b/tests/integration/tools/mongodb/mongodbTool.test.ts @@ -19,7 +19,7 @@ import { setupMongoDBIntegrationTest } from "./mongodbHelpers.js"; import { ErrorCodes } from "../../../../src/common/errors.js"; import { Keychain } from "../../../../src/common/keychain.js"; import { Elicitation } from "../../../../src/elicitation.js"; -import { MongoDbTools } from "../../../../src/tools/index.js"; +import * as MongoDbTools from "../../../../src/tools/mongodb/tools.js"; import { VectorSearchEmbeddingsManager } from "../../../../src/common/search/vectorSearchEmbeddingsManager.js"; const injectedErrorHandler: ConnectionErrorHandler = (error) => { diff --git a/tests/unit/toolBase.test.ts b/tests/unit/toolBase.test.ts index 82747955..b5dc928c 100644 --- a/tests/unit/toolBase.test.ts +++ b/tests/unit/toolBase.test.ts @@ -60,6 +60,7 @@ describe("ToolBase", () => { } as unknown as Elicitation; const constructorParams: ToolConstructorParams = { + category: TestTool.category, operationType: TestTool.operationType, session: mockSession, config: mockConfig, @@ -264,7 +265,7 @@ describe("ToolBase", () => { class TestTool extends ToolBase { public name = "test-tool"; - public category: ToolCategory = "mongodb"; + static category: ToolCategory = "mongodb"; static operationType: OperationType = "delete"; protected description = "A test tool for verification tests"; protected argsShape = { From 18f8da360abf968e0808a28a3b2ceab1f8f3b3c8 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 1 Dec 2025 17:36:45 +0100 Subject: [PATCH 6/7] chore: add test for export reliance --- tests/integration/build.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts index e647956a..8ac3675f 100644 --- a/tests/integration/build.test.ts +++ b/tests/integration/build.test.ts @@ -70,6 +70,7 @@ describe("Build Test", () => { const esmKeys = Object.keys(esmModule).sort(); expect(cjsKeys).toEqual(esmKeys); - expect(cjsKeys).toEqual(expect.arrayContaining(["MongoDbTools", "AtlasTools", "AtlasLocalTools", "AllTools"])); + // There are more tools but we will only check for a few. + expect(cjsKeys).toEqual(expect.arrayContaining(["AllTools", "AggregateTool", "FindTool"])); }); }); From c786c6b2a3500a774a8687d9de299636a1c97a23 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 1 Dec 2025 17:37:50 +0100 Subject: [PATCH 7/7] chore: add test for export reliance --- tests/integration/tools/tools.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/integration/tools/tools.test.ts diff --git a/tests/integration/tools/tools.test.ts b/tests/integration/tools/tools.test.ts new file mode 100644 index 00000000..24dbc0dd --- /dev/null +++ b/tests/integration/tools/tools.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { AllTools, ToolBase } from "../../../src/tools/index.js"; + +describe("all exported tools", () => { + it("'AllTools' should be a list of ToolBase implementations", () => { + expect(AllTools).toBeInstanceOf(Array); + AllTools.forEach((toolCtor) => { + expect(Object.prototype.isPrototypeOf.call(ToolBase, toolCtor)).toBe(true); + }); + }); + + it("each tool in 'AllTools' list should have required static properties for ToolClass conformance", () => { + AllTools.forEach((toolCtor) => { + expect(toolCtor).toHaveProperty("category"); + expect(toolCtor).toHaveProperty("operationType"); + }); + }); +});