Skip to content

Commit 02881f9

Browse files
chore: disallow overriding of internal tool impl
1 parent 313ee7f commit 02881f9

File tree

5 files changed

+65
-26
lines changed

5 files changed

+65
-26
lines changed

src/server.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ export interface ServerOptions {
3131
elicitation: Elicitation;
3232
connectionErrorHandler: ConnectionErrorHandler;
3333
/**
34-
* Custom tool constructors to register with the server.
35-
* This will override any default tools. You can use both existing and custom tools by using the `mongodb-mcp-server/tools` export.
34+
* Custom tool constructors to register with the server. The tools provided
35+
* here will be registered in addition to the default tools.
3636
*
3737
* ```ts
38-
* import { AllTools, ToolBase } from "mongodb-mcp-server/tools";
38+
* import { ToolBase } from "mongodb-mcp-server/tools";
3939
* class CustomTool extends ToolBase {
4040
* name = "custom_tool";
4141
* // ...
@@ -47,11 +47,11 @@ export interface ServerOptions {
4747
* telemetry: myTelemetry,
4848
* elicitation: myElicitation,
4949
* connectionErrorHandler: myConnectionErrorHandler,
50-
* tools: [...AllTools, CustomTool],
50+
* additionalTools: [CustomTool],
5151
* });
5252
* ```
5353
*/
54-
tools?: (new (params: ToolConstructorParams) => ToolBase)[];
54+
additionalTools?: (new (params: ToolConstructorParams) => ToolBase)[];
5555
}
5656

5757
export class Server {
@@ -60,7 +60,8 @@ export class Server {
6060
private readonly telemetry: Telemetry;
6161
public readonly userConfig: UserConfig;
6262
public readonly elicitation: Elicitation;
63-
private readonly toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[];
63+
private readonly internalToolImplementations: (new (params: ToolConstructorParams) => ToolBase)[] = AllTools;
64+
private readonly additionalToolImplementations: (new (params: ToolConstructorParams) => ToolBase)[];
6465
public readonly tools: ToolBase[] = [];
6566
public readonly connectionErrorHandler: ConnectionErrorHandler;
6667

@@ -80,7 +81,7 @@ export class Server {
8081
telemetry,
8182
connectionErrorHandler,
8283
elicitation,
83-
tools,
84+
additionalTools,
8485
}: ServerOptions) {
8586
this.startTime = Date.now();
8687
this.session = session;
@@ -89,7 +90,7 @@ export class Server {
8990
this.userConfig = userConfig;
9091
this.elicitation = elicitation;
9192
this.connectionErrorHandler = connectionErrorHandler;
92-
this.toolConstructors = tools ?? AllTools;
93+
this.additionalToolImplementations = additionalTools ?? [];
9394
}
9495

9596
async connect(transport: Transport): Promise<void> {
@@ -240,15 +241,37 @@ export class Server {
240241
}
241242

242243
private registerTools(): void {
243-
for (const toolConstructor of this.toolConstructors) {
244+
const toolImplementations = [
245+
...this.internalToolImplementations.map((toolConstructor) => ({
246+
toolConstructor,
247+
source: "internal",
248+
})),
249+
...this.additionalToolImplementations.map((toolConstructor) => ({
250+
toolConstructor,
251+
source: "additional",
252+
})),
253+
] as const;
254+
const registeredTools: Set<string> = new Set();
255+
256+
for (const { source, toolConstructor } of toolImplementations) {
244257
const tool = new toolConstructor({
245258
session: this.session,
246259
config: this.userConfig,
247260
telemetry: this.telemetry,
248261
elicitation: this.elicitation,
249262
});
263+
264+
if (registeredTools.has(tool.name)) {
265+
throw new Error(
266+
source === "internal"
267+
? `Tool name collision detected for internal tool - '${tool.name}'`
268+
: `Tool name collision detected for additional tool - '${tool.name}'. Cannot register an additional tool with the same name as that of an internal tool.`
269+
);
270+
}
271+
250272
if (tool.register(this)) {
251273
this.tools.push(tool);
274+
registeredTools.add(tool.name);
252275
}
253276
}
254277
}

src/transports/base.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type TransportRunnerConfig = {
4040
createAtlasLocalClient?: AtlasLocalClientFactoryFn;
4141
additionalLoggers?: LoggerBase[];
4242
telemetryProperties?: Partial<CommonProperties>;
43-
tools?: (new (params: ToolConstructorParams) => ToolBase)[];
43+
additionalTools?: (new (params: ToolConstructorParams) => ToolBase)[];
4444
/**
4545
* Hook which allows library consumers to fetch configuration from external sources (e.g., secrets managers, APIs)
4646
* or modify the existing configuration before the session is created.
@@ -56,7 +56,7 @@ export abstract class TransportRunnerBase {
5656
private readonly connectionErrorHandler: ConnectionErrorHandler;
5757
private readonly atlasLocalClient: Promise<Client | undefined>;
5858
private readonly telemetryProperties: Partial<CommonProperties>;
59-
private readonly tools?: (new (params: ToolConstructorParams) => ToolBase)[];
59+
private readonly additionalTools?: (new (params: ToolConstructorParams) => ToolBase)[];
6060
private readonly createSessionConfig?: CreateSessionConfigFn;
6161

6262
protected constructor({
@@ -66,15 +66,15 @@ export abstract class TransportRunnerBase {
6666
createAtlasLocalClient = defaultCreateAtlasLocalClient,
6767
additionalLoggers = [],
6868
telemetryProperties = {},
69-
tools,
69+
additionalTools,
7070
createSessionConfig,
7171
}: TransportRunnerConfig) {
7272
this.userConfig = userConfig;
7373
this.createConnectionManager = createConnectionManager;
7474
this.connectionErrorHandler = connectionErrorHandler;
7575
this.atlasLocalClient = createAtlasLocalClient();
7676
this.telemetryProperties = telemetryProperties;
77-
this.tools = tools;
77+
this.additionalTools = additionalTools;
7878
this.createSessionConfig = createSessionConfig;
7979
const loggers: LoggerBase[] = [...additionalLoggers];
8080
if (this.userConfig.loggers.includes("stderr")) {
@@ -149,7 +149,7 @@ export abstract class TransportRunnerBase {
149149
userConfig,
150150
connectionErrorHandler: this.connectionErrorHandler,
151151
elicitation,
152-
tools: this.tools,
152+
additionalTools: this.additionalTools,
153153
});
154154

155155
// We need to create the MCP logger after the server is constructed

tests/integration/customTools.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { defaultTestConfig, setupIntegrationTest } from "./helpers.js";
88
describe("Custom Tools", () => {
99
const { mcpClient, mcpServer } = setupIntegrationTest(() => ({ ...defaultTestConfig }), {
1010
serverOptions: {
11-
tools: [CustomGreetingTool, CustomCalculatorTool],
11+
additionalTools: [CustomGreetingTool, CustomCalculatorTool],
1212
},
1313
});
1414

15-
it("should register custom tools instead of default tools", async () => {
15+
it("should register custom tools in addition to default tools", async () => {
1616
// Check that custom tools are registered
1717
const tools = await mcpClient().listTools();
1818
const customGreetingTool = tools.tools.find((t) => t.name === "custom_greeting");
@@ -21,9 +21,9 @@ describe("Custom Tools", () => {
2121
expect(customGreetingTool).toBeDefined();
2222
expect(customCalculatorTool).toBeDefined();
2323

24-
// Check that default tools are NOT registered since we only provided custom tools
24+
// Check that default tools are also registered
2525
const defaultTool = tools.tools.find((t) => t.name === "list-databases");
26-
expect(defaultTool).toBeUndefined();
26+
expect(defaultTool).toBeDefined();
2727
});
2828

2929
it("should execute custom tools", async () => {

tests/integration/tools/mongodb/create/createIndex.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ describeWithMongoDB(
510510
expect(indexes).toHaveLength(1);
511511
expect(indexes[0]?.name).toEqual("vector_1_vector");
512512
expect(indexes[0]?.type).toEqual("vectorSearch");
513-
expect(indexes[0]?.status).toEqual("PENDING");
513+
expect(indexes[0]?.status).toEqual(expect.stringMatching(/PENDING|BUILDING/));
514514
expect(indexes[0]?.queryable).toEqual(false);
515515
expect(indexes[0]?.latestDefinition).toEqual({
516516
fields: [

tests/integration/tools/mongodb/mongodbTool.test.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { setupMongoDBIntegrationTest } from "./mongodbHelpers.js";
1919
import { ErrorCodes } from "../../../../src/common/errors.js";
2020
import { Keychain } from "../../../../src/common/keychain.js";
2121
import { Elicitation } from "../../../../src/elicitation.js";
22-
import { MongoDbTools } from "../../../../src/tools/mongodb/tools.js";
2322
import { VectorSearchEmbeddingsManager } from "../../../../src/common/search/vectorSearchEmbeddingsManager.js";
2423

2524
const injectedErrorHandler: ConnectionErrorHandler = (error) => {
@@ -89,7 +88,7 @@ describe("MongoDBTool implementations", () => {
8988

9089
async function cleanupAndStartServer(
9190
config: Partial<UserConfig> | undefined = {},
92-
toolConstructors: (new (params: ToolConstructorParams) => ToolBase)[] = [...MongoDbTools, RandomTool],
91+
additionalTools: (new (params: ToolConstructorParams) => ToolBase)[] = [RandomTool],
9392
errorHandler: ConnectionErrorHandler | undefined = connectionErrorHandler
9493
): Promise<void> {
9594
await cleanup();
@@ -140,7 +139,7 @@ describe("MongoDBTool implementations", () => {
140139
mcpServer: internalMcpServer,
141140
connectionErrorHandler: errorHandler,
142141
elicitation,
143-
tools: toolConstructors,
142+
additionalTools,
144143
});
145144

146145
await mcpServer.connect(serverTransport);
@@ -237,7 +236,7 @@ describe("MongoDBTool implementations", () => {
237236

238237
describe("when MCP is using injected connection error handler", () => {
239238
beforeEach(async () => {
240-
await cleanupAndStartServer(defaultTestConfig, [...MongoDbTools, RandomTool], injectedErrorHandler);
239+
await cleanupAndStartServer(defaultTestConfig, [RandomTool], injectedErrorHandler);
241240
});
242241

243242
describe("and comes across a MongoDB Error - NotConnectedToMongoDB", () => {
@@ -263,7 +262,7 @@ describe("MongoDBTool implementations", () => {
263262
// This is a misconfigured connection string
264263
await cleanupAndStartServer(
265264
{ connectionString: "mongodb://localhost:1234" },
266-
[...MongoDbTools, RandomTool],
265+
[RandomTool],
267266
injectedErrorHandler
268267
);
269268
const toolResponse = await mcpClient?.callTool({
@@ -287,7 +286,7 @@ describe("MongoDBTool implementations", () => {
287286
// This is a misconfigured connection string
288287
await cleanupAndStartServer(
289288
{ connectionString: mdbIntegration.connectionString(), indexCheck: true },
290-
[...MongoDbTools, RandomTool],
289+
[RandomTool],
291290
injectedErrorHandler
292291
);
293292
const toolResponse = await mcpClient?.callTool({
@@ -318,11 +317,28 @@ describe("MongoDBTool implementations", () => {
318317
injectedErrorHandler
319318
);
320319
const tools = await mcpClient?.listTools({});
321-
expect(tools?.tools).toHaveLength(1);
320+
expect(tools?.tools.find((tool) => tool.name === "Random")).toBeDefined();
322321
expect(tools?.tools.find((tool) => tool.name === "UnusableVoyageTool")).toBeUndefined();
323322
});
324323
});
325324

325+
describe("when an external tool with same name as that of internal tool is attempted to be registered", () => {
326+
it("server should throw during initialization itself", async () => {
327+
const serverStartPromise = cleanupAndStartServer(
328+
{ connectionString: mdbIntegration.connectionString(), indexCheck: true },
329+
[
330+
class SimilarRandomTool extends RandomTool {
331+
override name = "find";
332+
},
333+
],
334+
injectedErrorHandler
335+
);
336+
await expect(serverStartPromise).rejects.toThrow(
337+
"Tool name collision detected for additional tool - 'find'. Cannot register an additional tool with the same name as that of an internal tool."
338+
);
339+
});
340+
});
341+
326342
describe("resolveTelemetryMetadata", () => {
327343
it("should return empty metadata when not connected", async () => {
328344
await cleanupAndStartServer();

0 commit comments

Comments
 (0)