import { streamText, convertToModelMessages, isStepCount, smoothStream, toUIMessageStream, createUIMessageStreamResponse } from 'ai'import type { ToolSet } from 'ai'import { createMCPClient } from '@ai-sdk/mcp'import type { H3Event } from 'h3' const MAX_STEPS = 10 function createLocalFetch(event: H3Event): typeof fetch { const origin = getRequestURL(event).origin return (input, init) => { const requestUrl = input instanceof URL ? input : typeof input === 'string' ? new URL(input, origin) : new URL(input.url) const localPath = requestUrl.origin === origin ? `${requestUrl.pathname}${requestUrl.search}` : requestUrl.toString() return event.fetch(localPath, init) }} function getSystemPrompt(siteName: string) { return `You are the documentation assistant for ${siteName}. Help users navigate and understand the project documentation. **Your identity:**- You are an assistant helping users with ${siteName} documentation- NEVER use first person ("I", "me", "my") - always refer to the project by name: "${siteName} provides...", "${siteName} supports...", "The project offers..."- Be confident and knowledgeable about the project- Speak as a helpful guide, not as the documentation itself **Tool usage (CRITICAL):**- You have tools: list-pages (discover pages) and get-page (read a page)- If a page title clearly matches the question, read it directly without listing first- ALWAYS respond with text after using tools - never end with just tool calls **Guidelines:**- If you can't find something, say "There is no documentation on that yet" or "${siteName} doesn't cover that topic yet"- Be concise, helpful, and direct- Guide users like a friendly expert would **Links and exploration:**- Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click- When it helps, add extra links (related pages, "read more", side topics) — make the answer easy to dig into, not a wall of text- Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid **FORMATTING RULES (CRITICAL):**- NEVER use markdown headings (#, ##, ###, etc.)- Use **bold text** for emphasis and section labels- Start responses with content directly, never with a heading- Use bullet points for lists- Keep code examples focused and minimal **Response style:**- Conversational but professional- "Here's how you can do that:" instead of "The documentation shows:"- "${siteName} supports TypeScript out of the box" instead of "I support TypeScript"- Provide actionable guidance, not just information dumps`} export default defineEventHandler(async (event) => { const { messages } = await readBody(event) const config = useRuntimeConfig() const siteConfig = getSiteConfig(event) const siteName = siteConfig.name || 'Documentation' const mcpServer = config.assistant.mcpServer const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://') const baseURL = config.app?.baseURL?.replace(/\/$/, '') || '' const abortController = new AbortController() event.node.req.on('close', () => abortController.abort()) let transport: Parameters<typeof createMCPClient>[0]['transport'] if (isExternalUrl) { transport = { type: 'http', url: mcpServer, } } else if (import.meta.dev) { transport = { type: 'http', url: `http://localhost:3000${baseURL}${mcpServer}`, } } else { transport = { type: 'http', url: `${getRequestURL(event).origin}${baseURL}${mcpServer}`, fetch: createLocalFetch(event), } } const httpClient = await createMCPClient({ transport }) const mcpTools = await httpClient.tools() const closeMcp = () => event.waitUntil(httpClient.close()) const result = streamText({ model: config.assistant.model, maxOutputTokens: 8000, maxRetries: 2, abortSignal: abortController.signal, stopWhen: isStepCount(MAX_STEPS), // On the last allowed step, disable tools so the model is forced to // produce a final text answer instead of stopping mid tool-calling. prepareStep: ({ stepNumber }) => { return stepNumber >= MAX_STEPS - 1 ? { toolChoice: 'none' } : {} }, providerOptions: { gateway: { caching: 'auto', }, }, instructions: getSystemPrompt(siteName), messages: await convertToModelMessages(messages), tools: mcpTools as ToolSet, experimental_transform: smoothStream(), onEnd: closeMcp, onAbort: closeMcp, onError: closeMcp, }) return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }), })})
import { streamText, convertToModelMessages, isStepCount, smoothStream, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import type { ToolSet } from 'ai'
import { createMCPClient } from '@ai-sdk/mcp'
import type { H3Event } from 'h3'
const MAX_STEPS = 10
function createLocalFetch(event: H3Event): typeof fetch {
const origin = getRequestURL(event).origin
return (input, init) => {
const requestUrl = input instanceof URL
? input
: typeof input === 'string'
? new URL(input, origin)
: new URL(input.url)
const localPath = requestUrl.origin === origin
? `${requestUrl.pathname}${requestUrl.search}`
: requestUrl.toString()
return event.fetch(localPath, init)
}
}
function getSystemPrompt(siteName: string) {
return `You are the documentation assistant for ${siteName}. Help users navigate and understand the project documentation.
**Your identity:**
- You are an assistant helping users with ${siteName} documentation
- NEVER use first person ("I", "me", "my") - always refer to the project by name: "${siteName} provides...", "${siteName} supports...", "The project offers..."
- Be confident and knowledgeable about the project
- Speak as a helpful guide, not as the documentation itself
**Tool usage (CRITICAL):**
- You have tools: list-pages (discover pages) and get-page (read a page)
- If a page title clearly matches the question, read it directly without listing first
- ALWAYS respond with text after using tools - never end with just tool calls
**Guidelines:**
- If you can't find something, say "There is no documentation on that yet" or "${siteName} doesn't cover that topic yet"
- Be concise, helpful, and direct
- Guide users like a friendly expert would
**Links and exploration:**
- Tool results include a \`url\` for each page — prefer markdown links \`[label](url)\` so users can open the doc in one click
- When it helps, add extra links (related pages, "read more", side topics) — make the answer easy to dig into, not a wall of text
- Stick to URLs from tool results (\`url\` / \`path\`) so links stay valid
**FORMATTING RULES (CRITICAL):**
- NEVER use markdown headings (#, ##, ###, etc.)
- Use **bold text** for emphasis and section labels
- Start responses with content directly, never with a heading
- Use bullet points for lists
- Keep code examples focused and minimal
**Response style:**
- Conversational but professional
- "Here's how you can do that:" instead of "The documentation shows:"
- "${siteName} supports TypeScript out of the box" instead of "I support TypeScript"
- Provide actionable guidance, not just information dumps`
}
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const config = useRuntimeConfig()
const siteConfig = getSiteConfig(event)
const siteName = siteConfig.name || 'Documentation'
const mcpServer = config.assistant.mcpServer
const isExternalUrl = mcpServer.startsWith('http://') || mcpServer.startsWith('https://')
const baseURL = config.app?.baseURL?.replace(/\/$/, '') || ''
const abortController = new AbortController()
event.node.req.on('close', () => abortController.abort())
let transport: Parameters<typeof createMCPClient>[0]['transport']
if (isExternalUrl) {
transport = {
type: 'http',
url: mcpServer,
}
}
else if (import.meta.dev) {
transport = {
type: 'http',
url: `http://localhost:3000${baseURL}${mcpServer}`,
}
}
else {
transport = {
type: 'http',
url: `${getRequestURL(event).origin}${baseURL}${mcpServer}`,
fetch: createLocalFetch(event),
}
}
const httpClient = await createMCPClient({ transport })
const mcpTools = await httpClient.tools()
const closeMcp = () => event.waitUntil(httpClient.close())
const result = streamText({
model: config.assistant.model,
maxOutputTokens: 8000,
maxRetries: 2,
abortSignal: abortController.signal,
stopWhen: isStepCount(MAX_STEPS),
// On the last allowed step, disable tools so the model is forced to
// produce a final text answer instead of stopping mid tool-calling.
prepareStep: ({ stepNumber }) => {
return stepNumber >= MAX_STEPS - 1 ? { toolChoice: 'none' } : {}
},
providerOptions: {
gateway: {
caching: 'auto',
},
},
instructions: getSystemPrompt(siteName),
messages: await convertToModelMessages(messages),
tools: mcpTools as ToolSet,
experimental_transform: smoothStream(),
onEnd: closeMcp,
onAbort: closeMcp,
onError: closeMcp,
})
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
})
})