| 250 | } |
| 251 | |
| 252 | private requestWithTimeout<T>( |
| 253 | method: string, |
| 254 | params: unknown, |
| 255 | timeoutMs: number, |
| 256 | signal?: AbortSignal, |
| 257 | ): Promise<T> { |
| 258 | const id = this.nextId++; |
| 259 | const req: JsonRpcRequest = { jsonrpc: '2.0', id, method, params }; |
| 260 | |
| 261 | return new Promise<T>((resolve, reject) => { |
| 262 | const onAbort = (): void => { |
| 263 | const p = this.pending.get(id); |
| 264 | if (!p) return; |
| 265 | this.pending.delete(id); |
| 266 | if (p.timeoutHandle) clearTimeout(p.timeoutHandle); |
| 267 | reject(new Error(`MCP request '${method}' aborted`)); |
| 268 | }; |
| 269 | |
| 270 | const timeoutHandle = setTimeout(() => { |
| 271 | const p = this.pending.get(id); |
| 272 | if (!p) return; |
| 273 | this.pending.delete(id); |
| 274 | signal?.removeEventListener('abort', onAbort); |
| 275 | reject(new Error(`MCP request '${method}' to ${this.name} timed out after ${timeoutMs}ms`)); |
| 276 | }, timeoutMs); |
| 277 | |
| 278 | this.pending.set(id, { |
| 279 | resolve: (v: unknown) => { |
| 280 | signal?.removeEventListener('abort', onAbort); |
| 281 | resolve(v as T); |
| 282 | }, |
| 283 | reject: (e: Error) => { |
| 284 | signal?.removeEventListener('abort', onAbort); |
| 285 | reject(e); |
| 286 | }, |
| 287 | method, |
| 288 | timeoutHandle, |
| 289 | }); |
| 290 | |
| 291 | if (signal) { |
| 292 | if (signal.aborted) { |
| 293 | onAbort(); |
| 294 | return; |
| 295 | } |
| 296 | signal.addEventListener('abort', onAbort, { once: true }); |
| 297 | } |
| 298 | |
| 299 | this.send(req).catch((e) => { |
| 300 | this.pending.delete(id); |
| 301 | clearTimeout(timeoutHandle); |
| 302 | signal?.removeEventListener('abort', onAbort); |
| 303 | reject(e); |
| 304 | }); |
| 305 | }); |
| 306 | } |
| 307 | |
| 308 | private notify(method: string, params?: unknown): void { |
| 309 | const notif: JsonRpcNotification = { jsonrpc: '2.0', method, params }; |