返回 AiToEarn
mcp-sse.service.ts
根目录 / project / aitoearn-backend / libs / nest-mcp / src / services / mcp-sse.service.ts
1 import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2 import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
3 import { Inject, Injectable, Logger } from '@nestjs/common'
4 import { ApplicationConfig, ContextIdFactory, ModuleRef } from '@nestjs/core'
5 import { HttpAdapterFactory } from '../adapters'
6 import { McpOptions } from '../interfaces'
7 import { buildMcpCapabilities } from '../utils/capabilities-builder'
8 import { normalizeEndpoint } from '../utils/normalize-endpoint'
9 import { McpExecutorService } from './mcp-executor.service'
10 import { McpRegistryService } from './mcp-registry.service'
11 import { SsePingService } from './sse-ping.service'
12
13 @Injectable()
14 export class McpSseService {
15 private readonly logger = new Logger(McpSseService.name)
16
17 // Note: Currently, storing transports and servers makes it a requirement to have sticky sessions.
18
19 // Map to store active transports by session ID
20 private readonly transports = new Map<string, SSEServerTransport>()
21 // Map to store MCP server instances by session ID
22 private readonly mcpServers = new Map<string, McpServer>()
23
24 constructor(
25 @Inject('MCP_OPTIONS') private readonly options: McpOptions,
26 @Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
27 private readonly applicationConfig: ApplicationConfig,
28 private readonly moduleRef: ModuleRef,
29 private readonly toolRegistry: McpRegistryService,
30 @Inject(SsePingService) private readonly pingService: SsePingService,
31 ) {}
32
33 /**
34 * Initialize the SSE service and configure ping service
35 */
36 initialize() {
37 // Configure ping service with options
38 this.pingService.configure({
39 pingEnabled: this.options.sse?.pingEnabled, // Enable by default
40 pingIntervalMs: this.options.sse?.pingIntervalMs,
41 })
42 }
43
44 /**
45 * Create and manage SSE connection
46 */
47 async createSseConnection(
48 rawReq: any,
49 rawRes: any,
50 messagesEndpoint: string,
51 apiPrefix: string,
52 ): Promise<void> {
53 const adapter = HttpAdapterFactory.getAdapter(rawReq, rawRes)
54 const res = adapter.adaptResponse(rawRes)
55
56 // Create a new SSE transport instance
57 const transport = new SSEServerTransport(
58 normalizeEndpoint(
59 `${apiPrefix}/${this.applicationConfig.getGlobalPrefix()}/${messagesEndpoint}`,
60 ),
61 res.raw,
62 )
63 const sessionId = transport.sessionId
64
65 // Create a new MCP server instance with dynamic capabilities
66 const capabilities = buildMcpCapabilities(
67 this.mcpModuleId,
68 this.toolRegistry,
69 this.options,
70 )
71 this.logger.debug('Built MCP capabilities:', capabilities)
72
73 // Create a new MCP server for this session with dynamic capabilities
74 const mcpServer = new McpServer(
75 { name: this.options.name, version: this.options.version },
76 {
77 capabilities,
78 instructions: this.options.instructions || '',
79 },
80 )
81
82 // Store the transport and server for this session
83 this.transports.set(sessionId, transport)
84 this.mcpServers.set(sessionId, mcpServer)
85
86 // Register the connection with the ping service
87 this.pingService.registerConnection(sessionId, transport, res)
88
89 transport.onclose = () => {
90 // Clean up when the connection closes
91 this.transports.delete(sessionId)
92 this.mcpServers.delete(sessionId)
93 this.pingService.removeConnection(sessionId)
94 }
95
96 await mcpServer.connect(transport)
97 }
98
99 /**
100 * Handle message processing for SSE
101 */
102 async handleMessage(rawReq: any, rawRes: any, body: unknown): Promise<any> {
103 const adapter = HttpAdapterFactory.getAdapter(rawReq, rawRes)
104 const req = adapter.adaptRequest(rawReq)
105 const res = adapter.adaptResponse(rawRes)
106 const sessionId = req.query['sessionId'] as string
107 const transport = this.transports.get(sessionId)
108
109 if (!transport) {
110 return res.status(404).send('Session not found')
111 }
112
113 const mcpServer = this.mcpServers.get(sessionId)
114 if (!mcpServer) {
115 return res.status(404).send('MCP server not found for session')
116 }
117
118 // Resolve the request-scoped tool executor service
119 const contextId = ContextIdFactory.getByRequest(req)
120 const executor = await this.moduleRef.resolve(
121 McpExecutorService,
122 contextId,
123 )
124
125 // Register request handlers with the user context from this specific request
126 executor.registerRequestHandlers(mcpServer, req)
127
128 // Process the message
129 await transport.handlePostMessage(req.raw, res.raw, body)
130 }
131 }
132
132 lines TYPESCRIPT