返回 AiToEarn
mcp.module.ts
根目录 / project / aitoearn-backend / libs / nest-mcp / src / mcp.module.ts
1 import type {
2 McpAsyncOptions,
3 McpModuleAsyncOptions,
4 McpOptions,
5 McpOptionsFactory,
6 } from './interfaces'
7 import { DynamicModule, Module, Provider, Type } from '@nestjs/common'
8 import { DiscoveryModule } from '@nestjs/core'
9 import { McpTransportType } from './interfaces'
10 import { MCP_TOOL_MONITOR, McpToolMonitor } from './interfaces/mcp-tool-monitor.interface'
11 import { McpExecutorService } from './services/mcp-executor.service'
12 import { McpRegistryService } from './services/mcp-registry.service'
13 import { McpSseService } from './services/mcp-sse.service'
14 import { McpStreamableHttpService } from './services/mcp-streamable-http.service'
15 import { SsePingService } from './services/sse-ping.service'
16 import { createSseController } from './transport/sse.controller.factory'
17 import { createStreamableHttpController } from './transport/streamable-http.controller.factory'
18 import { normalizeEndpoint } from './utils/normalize-endpoint'
19
20 const DEFAULT_TOOL_MONITOR_PROVIDER: Provider = {
21 provide: MCP_TOOL_MONITOR,
22 useValue: {
23 async onToolSuccess() {
24 return undefined
25 },
26 async onToolError() {
27 return undefined
28 },
29 } satisfies McpToolMonitor,
30 }
31
32 let instanceIdCounter = 0
33
34 @Module({
35 imports: [DiscoveryModule],
36 providers: [McpRegistryService, McpExecutorService],
37 })
38 export class McpModule {
39 /**
40 * To avoid import circular dependency issues, we use a marker property.
41 */
42 readonly __isMcpModule = true
43
44 static forRoot(options: McpOptions): DynamicModule {
45 const defaultOptions: Partial<McpOptions> = {
46 transport: [
47 McpTransportType.SSE,
48 McpTransportType.STREAMABLE_HTTP,
49 McpTransportType.STDIO,
50 ],
51 sseEndpoint: 'sse',
52 messagesEndpoint: 'messages',
53 mcpEndpoint: 'mcp',
54 guards: [],
55 decorators: [],
56 streamableHttp: {
57 enableJsonResponse: true,
58 sessionIdGenerator: undefined,
59 statelessMode: true,
60 },
61 sse: {
62 pingEnabled: true,
63 pingIntervalMs: 30000,
64 },
65 }
66 const mergedOptions = { ...defaultOptions, ...options } as McpOptions
67 mergedOptions.sseEndpoint = normalizeEndpoint(mergedOptions.sseEndpoint)
68 mergedOptions.messagesEndpoint = normalizeEndpoint(
69 mergedOptions.messagesEndpoint,
70 )
71 mergedOptions.mcpEndpoint = normalizeEndpoint(mergedOptions.mcpEndpoint)
72
73 const moduleId = `mcp-module-${instanceIdCounter++}`
74 const providers = this.createProvidersFromOptions(mergedOptions, moduleId)
75 const controllers = this.createControllersFromOptions(mergedOptions)
76 return {
77 module: McpModule,
78 controllers,
79 providers,
80 exports: [McpRegistryService, McpSseService, McpStreamableHttpService],
81 }
82 }
83
84 /**
85 * Asynchronous variant of forRoot. Controllers are NOT auto-registered here because
86 * they must be declared synchronously at module definition time. This keeps the
87 * API explicit: when using forRootAsync, you are responsible for creating and
88 * registering any transport controllers (e.g. via createSseController / createStreamableHttpController).
89 *
90 * The exposed async options intentionally omit the `transport` property. Transport
91 * selection only influences automatic controller creation (which does not occur here)
92 * and STDIO auto-start. If you need STDIO with forRootAsync, manually instantiate
93 * and bootstrap it (e.g. by importing a module that injects StdioService) or add
94 * an explicit provider that sets options.transport before use.
95 */
96 static forRootAsync(options: McpModuleAsyncOptions): DynamicModule {
97 const moduleId = `mcp-module-${instanceIdCounter++}`
98 const asyncProviders = this.createAsyncProviders(options)
99 const baseProviders: Provider[] = [
100 {
101 provide: 'MCP_MODULE_ID',
102 useValue: moduleId,
103 },
104 DEFAULT_TOOL_MONITOR_PROVIDER,
105 McpRegistryService,
106 McpExecutorService,
107 SsePingService,
108 McpSseService,
109 McpStreamableHttpService,
110 ]
111
112 return {
113 module: McpModule,
114 imports: options.imports ?? [],
115 // No automatic controllers in async mode
116 controllers: [],
117 providers: [
118 ...asyncProviders,
119 ...baseProviders,
120 ...(options.extraProviders ?? []),
121 ],
122 exports: [McpRegistryService, McpSseService, McpStreamableHttpService],
123 }
124 }
125
126 private static createAsyncProviders(
127 options: McpModuleAsyncOptions,
128 ): Provider[] {
129 if (options.useFactory) {
130 return [
131 {
132 provide: 'MCP_OPTIONS',
133 useFactory: async (...args: unknown[]) => {
134 const resolved: McpAsyncOptions = await options.useFactory!(
135 ...args,
136 )
137 return this.mergeAndNormalizeAsyncOptions(resolved)
138 },
139 inject: options.inject ?? [],
140 },
141 ]
142 }
143
144 // useClass / useExisting path
145 const inject: any[] = []
146 let optionsFactoryProvider: Provider | undefined
147
148 if (options.useExisting || options.useClass) {
149 const useExisting = options.useExisting || options.useClass!
150 inject.push(useExisting)
151 if (options.useClass) {
152 optionsFactoryProvider = {
153 provide: options.useClass,
154 useClass: options.useClass,
155 } as Provider
156 }
157
158 return [
159 ...(optionsFactoryProvider ? [optionsFactoryProvider] : []),
160 {
161 provide: 'MCP_OPTIONS',
162 useFactory: async (factory: McpOptionsFactory) => {
163 const resolved = await factory.createMcpOptions()
164 return this.mergeAndNormalizeAsyncOptions(resolved)
165 },
166 inject,
167 },
168 ]
169 }
170
171 throw new Error('Invalid McpModuleAsyncOptions configuration.')
172 }
173
174 private static mergeAndNormalizeAsyncOptions(
175 resolved: McpAsyncOptions,
176 ): McpOptions {
177 const defaultOptions: Partial<McpOptions> = {
178 sseEndpoint: 'sse',
179 messagesEndpoint: 'messages',
180 mcpEndpoint: 'mcp',
181 guards: [],
182 decorators: [],
183 streamableHttp: {
184 enableJsonResponse: true,
185 sessionIdGenerator: undefined,
186 statelessMode: true,
187 },
188 sse: {
189 pingEnabled: true,
190 pingIntervalMs: 30000,
191 },
192 }
193 // Note: transport intentionally omitted
194 const merged = { ...defaultOptions, ...resolved } as McpOptions
195 merged.sseEndpoint = normalizeEndpoint(merged.sseEndpoint)
196 merged.messagesEndpoint = normalizeEndpoint(merged.messagesEndpoint)
197 merged.mcpEndpoint = normalizeEndpoint(merged.mcpEndpoint)
198 return merged
199 }
200
201 private static createControllersFromOptions(
202 options: McpOptions,
203 ): Type<any>[] {
204 const sseEndpoint = options.sseEndpoint ?? 'sse'
205 const messagesEndpoint = options.messagesEndpoint ?? 'messages'
206 const mcpEndpoint = options.mcpEndpoint ?? 'mcp'
207 const guards = options.guards ?? []
208 const transports = Array.isArray(options.transport)
209 ? options.transport
210 : [options.transport ?? McpTransportType.SSE]
211 const controllers: Type<any>[] = []
212 const decorators = options.decorators ?? []
213 const apiPrefix = options.apiPrefix ?? ''
214
215 if (transports.includes(McpTransportType.SSE)) {
216 const sseController = createSseController(
217 sseEndpoint,
218 messagesEndpoint,
219 apiPrefix,
220 guards,
221 decorators,
222 )
223 controllers.push(sseController)
224 }
225
226 if (transports.includes(McpTransportType.STREAMABLE_HTTP)) {
227 const streamableHttpController = createStreamableHttpController(
228 mcpEndpoint,
229 apiPrefix,
230 guards,
231 decorators,
232 )
233 controllers.push(streamableHttpController)
234 }
235
236 if (transports.includes(McpTransportType.STDIO)) {
237 // STDIO transport is handled by injectable StdioService, no controller
238 }
239
240 return controllers
241 }
242
243 private static createProvidersFromOptions(
244 options: McpOptions,
245 moduleId: string,
246 ): Provider[] {
247 const providers: Provider[] = [
248 {
249 provide: 'MCP_OPTIONS',
250 useValue: options,
251 },
252 {
253 provide: 'MCP_MODULE_ID',
254 useValue: moduleId,
255 },
256 options.toolMonitorProvider ?? DEFAULT_TOOL_MONITOR_PROVIDER,
257 McpRegistryService,
258 McpExecutorService,
259 SsePingService,
260 McpSseService,
261 McpStreamableHttpService,
262 ]
263
264 return providers
265 }
266 }
267
267 lines TYPESCRIPT