|
| 1 | +diff --git a/modules/assistant/runtime/server/api/search.ts b/modules/assistant/runtime/server/api/search.ts |
| 2 | +index 279508057d9fee9d7cc3a7f09cd8be5687e1c3e0..3a836d65bf7bc8e2dc41273463bdf9e7376d80f5 100644 |
| 3 | +--- a/modules/assistant/runtime/server/api/search.ts |
| 4 | ++++ b/modules/assistant/runtime/server/api/search.ts |
| 5 | +@@ -1,19 +1,93 @@ |
| 6 | +-import { streamText, convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse } from 'ai' |
| 7 | +-import type { UIMessageStreamWriter, ToolCallPart, ToolSet } from 'ai' |
| 8 | ++import type { ToolCallPart, ToolSet, UIMessageStreamWriter } from 'ai' |
| 9 | + import { createMCPClient } from '@ai-sdk/mcp' |
| 10 | ++import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, streamText } from 'ai' |
| 11 | ++import type { H3Event } from 'h3' |
| 12 | ++ |
| 13 | ++interface McpTransport { |
| 14 | ++ start(): Promise<void> |
| 15 | ++ close(): Promise<void> |
| 16 | ++ send(message: Record<string, unknown>): Promise<void> |
| 17 | ++ onmessage?: ((message: Record<string, unknown>) => void) | undefined |
| 18 | ++ onerror?: ((error: Error) => void) | undefined |
| 19 | ++ onclose?: (() => void) | undefined |
| 20 | ++} |
| 21 | + |
| 22 | + const MAX_STEPS = 10 |
| 23 | + |
| 24 | ++/** |
| 25 | ++ * MCP transport that routes through Nitro's internal localFetch (event.fetch) |
| 26 | ++ * instead of globalThis.$fetch (event.$fetch) which uses the global fetch() |
| 27 | ++ * and triggers CF Workers self-fetch error 1042. |
| 28 | ++ */ |
| 29 | ++function createInternalMcpTransport(event: H3Event, mcpPath: string): McpTransport { |
| 30 | ++ let _onmessage: ((message: Record<string, unknown>) => void) | undefined |
| 31 | ++ let _onerror: ((error: Error) => void) | undefined |
| 32 | ++ let _onclose: (() => void) | undefined |
| 33 | ++ |
| 34 | ++ return { |
| 35 | ++ async start() {}, |
| 36 | ++ async close() { _onclose?.() }, |
| 37 | ++ get onmessage() { return _onmessage }, |
| 38 | ++ set onmessage(fn) { _onmessage = fn }, |
| 39 | ++ get onerror() { return _onerror }, |
| 40 | ++ set onerror(fn) { _onerror = fn }, |
| 41 | ++ get onclose() { return _onclose }, |
| 42 | ++ set onclose(fn) { _onclose = fn }, |
| 43 | ++ async send(message: Record<string, unknown>) { |
| 44 | ++ try { |
| 45 | ++ const response = await event.fetch(mcpPath, { |
| 46 | ++ method: 'POST', |
| 47 | ++ body: JSON.stringify(message), |
| 48 | ++ headers: { |
| 49 | ++ 'Content-Type': 'application/json', |
| 50 | ++ 'Accept': 'application/json, text/event-stream', |
| 51 | ++ }, |
| 52 | ++ }) |
| 53 | ++ |
| 54 | ++ const contentType = response.headers.get('content-type') || '' |
| 55 | ++ |
| 56 | ++ if (contentType.includes('text/event-stream') && response.body) { |
| 57 | ++ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() |
| 58 | ++ let buffer = '' |
| 59 | ++ while (true) { |
| 60 | ++ const { done, value } = await reader.read() |
| 61 | ++ if (done) break |
| 62 | ++ buffer += value |
| 63 | ++ const lines = buffer.split('\n') |
| 64 | ++ buffer = lines.pop() || '' |
| 65 | ++ for (const line of lines) { |
| 66 | ++ if (line.startsWith('data: ')) |
| 67 | ++ _onmessage?.(JSON.parse(line.slice(6))) |
| 68 | ++ } |
| 69 | ++ } |
| 70 | ++ if (buffer.startsWith('data: ')) |
| 71 | ++ _onmessage?.(JSON.parse(buffer.slice(6))) |
| 72 | ++ } |
| 73 | ++ else { |
| 74 | ++ const body = await response.json() |
| 75 | ++ const messages = Array.isArray(body) ? body : [body] |
| 76 | ++ for (const m of messages) _onmessage?.(m as Record<string, unknown>) |
| 77 | ++ } |
| 78 | ++ } |
| 79 | ++ catch (error) { |
| 80 | ++ _onerror?.(error as Error) |
| 81 | ++ } |
| 82 | ++ }, |
| 83 | ++ } |
| 84 | ++} |
| 85 | ++ |
| 86 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 87 | + function stopWhenResponseComplete({ steps }: { steps: any[] }): boolean { |
| 88 | + const lastStep = steps.at(-1) |
| 89 | +- if (!lastStep) return false |
| 90 | ++ if (!lastStep) |
| 91 | ++ return false |
| 92 | + |
| 93 | + // Primary condition: stop when model gives a text response without tool calls |
| 94 | + const hasText = Boolean(lastStep.text && lastStep.text.trim().length > 0) |
| 95 | + const hasNoToolCalls = !lastStep.toolCalls || lastStep.toolCalls.length === 0 |
| 96 | + |
| 97 | +- if (hasText && hasNoToolCalls) return true |
| 98 | ++ if (hasText && hasNoToolCalls) |
| 99 | ++ return true |
| 100 | + |
| 101 | + return steps.length >= MAX_STEPS |
| 102 | + } |
| 103 | +@@ -60,16 +134,14 @@ export default defineEventHandler(async (event) => { |
| 104 | + |
| 105 | + const mcpServer = config.assistant.mcpServer |
| 106 | + const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://') |
| 107 | +- const baseURL = config.app?.baseURL?.replace(/\/$/, '') || '' |
| 108 | +- const mcpUrl = isExternalUrl |
| 109 | +- ? mcpServer |
| 110 | ++ |
| 111 | ++ const transport = isExternalUrl |
| 112 | ++ ? { type: 'http' as const, url: mcpServer } |
| 113 | + : import.meta.dev |
| 114 | +- ? `http://localhost:3000${baseURL}${mcpServer}` |
| 115 | +- : `${getRequestURL(event).origin}${baseURL}${mcpServer}` |
| 116 | ++ ? { type: 'http' as const, url: `http://localhost:3000${mcpServer}` } |
| 117 | ++ : createInternalMcpTransport(event, mcpServer) |
| 118 | + |
| 119 | +- const httpClient = await createMCPClient({ |
| 120 | +- transport: { type: 'http', url: mcpUrl }, |
| 121 | +- }) |
| 122 | ++ const httpClient = await createMCPClient({ transport }) |
| 123 | + const mcpTools = await httpClient.tools() |
| 124 | + |
| 125 | + const stream = createUIMessageStream({ |
| 126 | +@@ -84,7 +156,8 @@ export default defineEventHandler(async (event) => { |
| 127 | + messages: modelMessages, |
| 128 | + tools: mcpTools as ToolSet, |
| 129 | + onStepFinish: ({ toolCalls }: { toolCalls: ToolCallPart[] }) => { |
| 130 | +- if (toolCalls.length === 0) return |
| 131 | ++ if (toolCalls.length === 0) |
| 132 | ++ return |
| 133 | + writer.write({ |
| 134 | + id: toolCalls[0]?.toolCallId, |
| 135 | + type: 'data-tool-calls', |
0 commit comments