(opts: McpHttpHandlerOptions)
| 105 | } |
| 106 | |
| 107 | export function createMcpHttpHandler(opts: McpHttpHandlerOptions): McpHttpHandler { |
| 108 | const sessions = new Map<string, McpHttpSession>(); |
| 109 | const sessionTtlMs = opts.sessionTtlMs ?? 30 * 60 * 1000; |
| 110 | const maxSessions = opts.maxSessions ?? 100; |
| 111 | |
| 112 | async function closeSession(sessionId: string, reason: string): Promise<void> { |
| 113 | const session = sessions.get(sessionId); |
| 114 | if (!session) return; |
| 115 | sessions.delete(sessionId); |
| 116 | if (session.ttlTimer !== undefined) clearTimeout(session.ttlTimer); |
| 117 | const results = await Promise.allSettled([session.server.close(), session.transport.close()]); |
| 118 | for (const result of results) { |
| 119 | if (result.status === 'rejected') { |
| 120 | opts.log?.warn?.( |
| 121 | { err: result.reason, sessionId, reason }, |
| 122 | 'MCP HTTP session close failed', |
| 123 | ); |
| 124 | } |
| 125 | } |
| 126 | opts.log?.info?.({ sessionId, reason }, 'MCP HTTP session closed'); |
| 127 | } |
| 128 | |
| 129 | function touchSession(sessionId: string, session: McpHttpSession): void { |
| 130 | if (session.ttlTimer !== undefined) clearTimeout(session.ttlTimer); |
| 131 | session.ttlTimer = setTimeout(() => { |
| 132 | void closeSession(sessionId, 'ttl-expired').catch((err) => { |
| 133 | opts.log?.warn?.({ err, sessionId }, 'MCP HTTP session TTL cleanup failed'); |
| 134 | }); |
| 135 | }, sessionTtlMs); |
| 136 | session.ttlTimer.unref?.(); |
| 137 | } |
| 138 | |
| 139 | return { |
| 140 | async handle(req, res): Promise<void> { |
| 141 | const sessionId = firstHeader(req.headers['mcp-session-id']); |
| 142 | if (sessionId) { |
| 143 | const session = sessions.get(sessionId); |
| 144 | if (!session) { |
| 145 | writePlain(res, 404, 'MCP session not found'); |
| 146 | return; |
| 147 | } |
| 148 | touchSession(sessionId, session); |
| 149 | await session.transport.handleRequest(req, res); |
| 150 | return; |
| 151 | } |
| 152 | |
| 153 | if (req.method !== 'POST') { |
| 154 | writePlain(res, 400, 'Missing MCP session. Initialize with POST /mcp first.'); |
| 155 | return; |
| 156 | } |
| 157 | if (sessions.size >= maxSessions) { |
| 158 | opts.log?.warn?.( |
| 159 | { activeSessions: sessions.size, maxSessions }, |
| 160 | 'MCP HTTP session cap reached', |
| 161 | ); |
| 162 | writePlain(res, 503, 'Too many active MCP sessions'); |
| 163 | return; |
| 164 | } |
no outgoing calls