Skip to content

Commit 8ed8dae

Browse files
authored
Scope discovery to subtree of module that imports McpServer (#73)
This allows for multiple servers to run within a single Nest app, each having its own separate set of tools. This is a breaking change - previously all tools were discovered regardless of where the McpModule was imported.
1 parent 6104891 commit 8ed8dae

11 files changed

+424
-69
lines changed

src/mcp.module.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ import { StdioService } from './transport/stdio.service';
99
import { createStreamableHttpController } from './transport/streamable-http.controller.factory';
1010
import { normalizeEndpoint } from './utils/normalize-endpoint';
1111

12+
let instanceIdCounter = 0;
13+
1214
@Module({
1315
imports: [DiscoveryModule],
1416
providers: [McpRegistryService, McpExecutorService],
1517
})
1618
export class McpModule {
19+
/**
20+
* To avoid import circular dependency issues, we use a marker property.
21+
*/
22+
readonly __isMcpModule = true;
23+
1724
static forRoot(options: McpOptions): DynamicModule {
1825
const defaultOptions: Partial<McpOptions> = {
1926
transport: [
@@ -42,7 +49,9 @@ export class McpModule {
4249
mergedOptions.messagesEndpoint,
4350
);
4451
mergedOptions.mcpEndpoint = normalizeEndpoint(mergedOptions.mcpEndpoint);
45-
const providers = this.createProvidersFromOptions(mergedOptions);
52+
53+
const moduleId = `mcp-module-${instanceIdCounter++}`;
54+
const providers = this.createProvidersFromOptions(mergedOptions, moduleId);
4655
const controllers = this.createControllersFromOptions(mergedOptions);
4756

4857
return {
@@ -92,12 +101,19 @@ export class McpModule {
92101
return controllers;
93102
}
94103

95-
private static createProvidersFromOptions(options: McpOptions): Provider[] {
104+
private static createProvidersFromOptions(
105+
options: McpOptions,
106+
moduleId: string,
107+
): Provider[] {
96108
const providers: Provider[] = [
97109
{
98110
provide: 'MCP_OPTIONS',
99111
useValue: options,
100112
},
113+
{
114+
provide: 'MCP_MODULE_ID',
115+
useValue: moduleId,
116+
},
101117
McpRegistryService,
102118
McpExecutorService,
103119
];

src/services/handlers/mcp-prompts.handler.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Scope } from '@nestjs/common';
1+
import { Inject, Injectable, Scope } from '@nestjs/common';
22
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
33
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
44
import {
@@ -14,31 +14,37 @@ import { McpHandlerBase } from './mcp-handler.base';
1414

1515
@Injectable({ scope: Scope.REQUEST })
1616
export class McpPromptsHandler extends McpHandlerBase {
17-
constructor(moduleRef: ModuleRef, registry: McpRegistryService) {
17+
constructor(
18+
moduleRef: ModuleRef,
19+
registry: McpRegistryService,
20+
@Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
21+
) {
1822
super(moduleRef, registry, McpPromptsHandler.name);
1923
}
2024

2125
registerHandlers(mcpServer: McpServer, httpRequest: Request) {
22-
if (this.registry.getPrompts().length === 0) {
26+
if (this.registry.getPrompts(this.mcpModuleId).length === 0) {
2327
this.logger.debug('No prompts registered, skipping prompt handlers');
2428
return;
2529
}
2630
mcpServer.server.setRequestHandler(ListPromptsRequestSchema, () => {
2731
this.logger.debug('ListPromptsRequestSchema is being called');
2832

29-
const prompts = this.registry.getPrompts().map((prompt) => ({
30-
name: prompt.metadata.name,
31-
description: prompt.metadata.description,
32-
arguments: prompt.metadata.parameters
33-
? Object.entries(prompt.metadata.parameters.shape).map(
34-
([name, field]): PromptArgument => ({
35-
name,
36-
description: field.description,
37-
required: !field.isOptional(),
38-
}),
39-
)
40-
: [],
41-
}));
33+
const prompts = this.registry
34+
.getPrompts(this.mcpModuleId)
35+
.map((prompt) => ({
36+
name: prompt.metadata.name,
37+
description: prompt.metadata.description,
38+
arguments: prompt.metadata.parameters
39+
? Object.entries(prompt.metadata.parameters.shape).map(
40+
([name, field]): PromptArgument => ({
41+
name,
42+
description: field.description,
43+
required: !field.isOptional(),
44+
}),
45+
)
46+
: [],
47+
}));
4248

4349
return {
4450
prompts,
@@ -52,7 +58,7 @@ export class McpPromptsHandler extends McpHandlerBase {
5258

5359
try {
5460
const name = request.params.name;
55-
const promptInfo = this.registry.findPrompt(name);
61+
const promptInfo = this.registry.findPrompt(this.mcpModuleId, name);
5662

5763
if (!promptInfo) {
5864
throw new McpError(

src/services/handlers/mcp-resources.handler.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Scope } from '@nestjs/common';
1+
import { Inject, Injectable, Scope } from '@nestjs/common';
22
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
33
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
44
import {
@@ -13,12 +13,16 @@ import { McpHandlerBase } from './mcp-handler.base';
1313

1414
@Injectable({ scope: Scope.REQUEST })
1515
export class McpResourcesHandler extends McpHandlerBase {
16-
constructor(moduleRef: ModuleRef, registry: McpRegistryService) {
16+
constructor(
17+
moduleRef: ModuleRef,
18+
registry: McpRegistryService,
19+
@Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
20+
) {
1721
super(moduleRef, registry, McpResourcesHandler.name);
1822
}
1923

2024
registerHandlers(mcpServer: McpServer, httpRequest: Request) {
21-
if (this.registry.getResources().length === 0) {
25+
if (this.registry.getResources(this.mcpModuleId).length === 0) {
2226
this.logger.debug('No resources registered, skipping resource handlers');
2327
return;
2428
}
@@ -27,7 +31,7 @@ export class McpResourcesHandler extends McpHandlerBase {
2731
this.logger.debug('ListResourcesRequestSchema is being called');
2832
return {
2933
resources: this.registry
30-
.getResources()
34+
.getResources(this.mcpModuleId)
3135
.map((resources) => resources.metadata),
3236
};
3337
});
@@ -38,7 +42,10 @@ export class McpResourcesHandler extends McpHandlerBase {
3842
this.logger.debug('ReadResourceRequestSchema is being called');
3943

4044
const uri = request.params.uri;
41-
const resourceInfo = this.registry.findResourceByUri(uri);
45+
const resourceInfo = this.registry.findResourceByUri(
46+
this.mcpModuleId,
47+
uri,
48+
);
4249

4350
if (!resourceInfo) {
4451
throw new McpError(

src/services/handlers/mcp-tools.handler.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ListToolsRequestSchema,
66
McpError,
77
} from '@modelcontextprotocol/sdk/types.js';
8-
import { Injectable, Scope } from '@nestjs/common';
8+
import { Inject, Injectable, Scope } from '@nestjs/common';
99
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
1010
import { Request } from 'express';
1111
import { zodToJsonSchema } from 'zod-to-json-schema';
@@ -14,18 +14,22 @@ import { McpHandlerBase } from './mcp-handler.base';
1414

1515
@Injectable({ scope: Scope.REQUEST })
1616
export class McpToolsHandler extends McpHandlerBase {
17-
constructor(moduleRef: ModuleRef, registry: McpRegistryService) {
17+
constructor(
18+
moduleRef: ModuleRef,
19+
registry: McpRegistryService,
20+
@Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
21+
) {
1822
super(moduleRef, registry, McpToolsHandler.name);
1923
}
2024

2125
registerHandlers(mcpServer: McpServer, httpRequest: Request) {
22-
if (this.registry.getTools().length === 0) {
26+
if (this.registry.getTools(this.mcpModuleId).length === 0) {
2327
this.logger.debug('No tools registered, skipping tool handlers');
2428
return;
2529
}
2630

2731
mcpServer.server.setRequestHandler(ListToolsRequestSchema, () => {
28-
const tools = this.registry.getTools().map((tool) => {
32+
const tools = this.registry.getTools(this.mcpModuleId).map((tool) => {
2933
// Create base schema
3034
const toolSchema = {
3135
name: tool.metadata.name,
@@ -64,7 +68,10 @@ export class McpToolsHandler extends McpHandlerBase {
6468
async (request) => {
6569
this.logger.debug('CallToolRequestSchema is being called');
6670

67-
const toolInfo = this.registry.findTool(request.params.name);
71+
const toolInfo = this.registry.findTool(
72+
this.mcpModuleId,
73+
request.params.name,
74+
);
6875

6976
if (!toolInfo) {
7077
throw new McpError(

src/services/mcp-executor.service.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2-
import { Injectable, Logger, Scope } from '@nestjs/common';
2+
import { Inject, Injectable, Logger, Scope } from '@nestjs/common';
33
import { ModuleRef } from '@nestjs/core';
44
import { Request } from 'express';
55
import { McpRegistryService } from './mcp-registry.service';
@@ -17,10 +17,22 @@ export class McpExecutorService {
1717
private resourcesHandler: McpResourcesHandler;
1818
private promptsHandler: McpPromptsHandler;
1919

20-
constructor(moduleRef: ModuleRef, registry: McpRegistryService) {
21-
this.toolsHandler = new McpToolsHandler(moduleRef, registry);
22-
this.resourcesHandler = new McpResourcesHandler(moduleRef, registry);
23-
this.promptsHandler = new McpPromptsHandler(moduleRef, registry);
20+
constructor(
21+
moduleRef: ModuleRef,
22+
registry: McpRegistryService,
23+
@Inject('MCP_MODULE_ID') mcpModuleId: string,
24+
) {
25+
this.toolsHandler = new McpToolsHandler(moduleRef, registry, mcpModuleId);
26+
this.resourcesHandler = new McpResourcesHandler(
27+
moduleRef,
28+
registry,
29+
mcpModuleId,
30+
);
31+
this.promptsHandler = new McpPromptsHandler(
32+
moduleRef,
33+
registry,
34+
mcpModuleId,
35+
);
2436
}
2537

2638
/**

0 commit comments

Comments
 (0)