Skip to content

Commit 897242e

Browse files
committed
fix(assistant): apply MCP transport patch via pnpm
1 parent 902f6e7 commit 897242e

File tree

3 files changed

+146
-3
lines changed

3 files changed

+146
-3
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@
9292
"workerd",
9393
"@parcel/watcher",
9494
"better-sqlite3"
95-
]
95+
],
96+
"patchedDependencies": {
97+
"docus@5.7.0": "patches/docus@5.7.0.patch"
98+
}
9699
},
97100
"simple-git-hooks": {
98101
"pre-commit": "pnpm lint-staged"

patches/docus@5.7.0.patch

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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',

pnpm-lock.yaml

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)