/components/ai-chat.tsx- frontend for the chatbot/components/app-sidebar.tsx- Right sidebar for the chatbot , add as<AppSidebar side="right" />in<SidebarProvider>/app/api/chat/route.ts- backend route connecting the MCP serverspackage.json- match the needed libs.env.local- match the environment vars (will be sent personally)
Last active
May 19, 2026 10:52
-
-
Save RayyanNafees/c717ccac45851b46b70535a0da322dc0 to your computer and use it in GitHub Desktop.
Bespokible AI chatbot UI snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { | |
| Attachment, | |
| AttachmentPreview, | |
| AttachmentRemove, | |
| Attachments, | |
| } from "@/components/ai-elements/attachments"; | |
| import { | |
| PromptInput, | |
| PromptInputActionAddAttachments, | |
| PromptInputActionAddScreenshot, | |
| PromptInputActionMenu, | |
| PromptInputActionMenuContent, | |
| PromptInputActionMenuTrigger, | |
| PromptInputBody, | |
| PromptInputButton, | |
| PromptInputHeader, | |
| type PromptInputMessage, | |
| PromptInputSelect, | |
| PromptInputSelectContent, | |
| PromptInputSelectItem, | |
| PromptInputSelectTrigger, | |
| PromptInputSelectValue, | |
| PromptInputSubmit, | |
| PromptInputTextarea, | |
| PromptInputFooter, | |
| PromptInputTools, | |
| usePromptInputAttachments, | |
| } from "@/components/ai-elements/prompt-input"; | |
| import { GlobeIcon } from "lucide-react"; | |
| import { useState, useEffect } from "react"; | |
| import { useChat } from "@ai-sdk/react"; | |
| import { | |
| Conversation, | |
| ConversationContent, | |
| ConversationScrollButton, | |
| } from "@/components/ai-elements/conversation"; | |
| import { | |
| Message, | |
| MessageContent, | |
| MessageResponse, | |
| } from "@/components/ai-elements/message"; | |
| import { | |
| DefaultChatTransport, | |
| lastAssistantMessageIsCompleteWithToolCalls, | |
| } from "ai"; | |
| const PromptInputAttachmentsDisplay = () => { | |
| const attachments = usePromptInputAttachments(); | |
| if (attachments.files.length === 0) { | |
| return null; | |
| } | |
| return ( | |
| <Attachments variant="inline"> | |
| {attachments.files.map((attachment) => ( | |
| <Attachment | |
| data={attachment} | |
| key={attachment.id} | |
| onRemove={() => attachments.remove(attachment.id)} | |
| > | |
| <AttachmentPreview /> | |
| <AttachmentRemove /> | |
| </Attachment> | |
| ))} | |
| </Attachments> | |
| ); | |
| }; | |
| const models = [ | |
| { id: "gpt-4o", name: "GPT-4o" }, | |
| { id: "claude-opus-4-20250514", name: "Claude 4 Opus" }, | |
| ]; | |
| const InputDemo = () => { | |
| const [text, setText] = useState<string>(""); | |
| const [model, setModel] = useState<string>(models[0].id); | |
| const [useWebSearch, setUseWebSearch] = useState<boolean>(false); | |
| // const [apiKey, setApiKey] = useState<string>(""); | |
| // useEffect(() => { | |
| // if (process.env.NEXT_PUBLIC_BESPOKIBLE_API_KEY) { | |
| // setApiKey(process.env.NEXT_PUBLIC_BESPOKIBLE_API_KEY); | |
| // } | |
| // }, []); | |
| // console.log(process.env.NEXT_PUBLIC_BESPOKIBLE_API_URL); | |
| const { messages, status, sendMessage, addToolOutput } = useChat({ | |
| sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, | |
| async onToolCall({ toolCall }) { | |
| // Check if it's a dynamic tool first for proper type narrowing | |
| if (toolCall.dynamic) { | |
| return; | |
| } | |
| // const cities = ["New York", "Los Angeles", "Chicago", "San Francisco"]; | |
| // No await - avoids potential deadlocks | |
| addToolOutput({ | |
| tool: toolCall.toolName, | |
| toolCallId: toolCall.toolCallId, | |
| output: toolCall.input, | |
| }); | |
| }, | |
| // transport: new DefaultChatTransport({ | |
| // api: process.env.NEXT_PUBLIC_BESPOKIBLE_API_URL as string, // "https://mcp.bespokible.com/chat", | |
| // headers: { | |
| // "X-API-KEY": process.env.NEXT_PUBLIC_BESPOKIBLE_API_KEY as string, | |
| // }, | |
| // }), | |
| }); | |
| useEffect(() => { | |
| document.body.scrollBy(1000, 1000); | |
| }, [messages]); | |
| const handleSubmit = (message: PromptInputMessage) => { | |
| const hasText = Boolean(message.text); | |
| const hasAttachments = Boolean(message.files?.length); | |
| if (!(hasText || hasAttachments)) { | |
| return; | |
| } | |
| sendMessage( | |
| { | |
| text: message.text || "Sent with attachments", | |
| files: message.files, | |
| }, | |
| { | |
| body: { | |
| model: model, | |
| webSearch: useWebSearch, | |
| }, | |
| }, | |
| ); | |
| setText(""); | |
| }; | |
| return ( | |
| <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border min-h-150"> | |
| <div className="flex flex-col h-full"> | |
| <Conversation> | |
| <ConversationContent> | |
| {messages.map((message) => ( | |
| <Message from={message.role} key={message.id}> | |
| <MessageContent> | |
| {message.parts.map((part, i) => { | |
| switch (part.type) { | |
| case "text": | |
| return ( | |
| <MessageResponse key={`${message.id}-${i}`}> | |
| {part.text} | |
| </MessageResponse> | |
| ); | |
| default: | |
| return null; | |
| } | |
| })} | |
| </MessageContent> | |
| </Message> | |
| ))} | |
| </ConversationContent> | |
| <ConversationScrollButton /> | |
| </Conversation> | |
| <PromptInput | |
| onSubmit={handleSubmit} | |
| // className="mt-auto absolute bottom-10" | |
| globalDrop | |
| multiple | |
| > | |
| <PromptInputHeader> | |
| <PromptInputAttachmentsDisplay /> | |
| </PromptInputHeader> | |
| <PromptInputBody> | |
| <PromptInputTextarea | |
| onChange={(e) => setText(e.target.value)} | |
| value={text} | |
| /> | |
| </PromptInputBody> | |
| <PromptInputFooter> | |
| <PromptInputTools> | |
| <PromptInputActionMenu> | |
| <PromptInputActionMenuTrigger /> | |
| <PromptInputActionMenuContent> | |
| <PromptInputActionAddAttachments /> | |
| <PromptInputActionAddScreenshot /> | |
| </PromptInputActionMenuContent> | |
| </PromptInputActionMenu> | |
| {/*<PromptInputButton | |
| onClick={() => setUseWebSearch(!useWebSearch)} | |
| tooltip={{ content: "Search the web", shortcut: "⌘K" }} | |
| variant={useWebSearch ? "default" : "ghost"} | |
| > | |
| <GlobeIcon size={16} /> | |
| <span>Search</span> | |
| </PromptInputButton>*/} | |
| <PromptInputSelect | |
| onValueChange={(value) => { | |
| setModel(value as string); | |
| }} | |
| value={model} | |
| > | |
| <PromptInputSelectTrigger> | |
| <PromptInputSelectValue /> | |
| </PromptInputSelectTrigger> | |
| <PromptInputSelectContent> | |
| {models.map((model) => ( | |
| <PromptInputSelectItem key={model.id} value={model.id}> | |
| {model.name} | |
| </PromptInputSelectItem> | |
| ))} | |
| </PromptInputSelectContent> | |
| </PromptInputSelect> | |
| </PromptInputTools> | |
| <PromptInputSubmit disabled={!text && !status} status={status} /> | |
| </PromptInputFooter> | |
| </PromptInput> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default InputDemo; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { AppSidebar } from "@/components/app-sidebar" | |
| import { | |
| Breadcrumb, | |
| BreadcrumbItem, | |
| BreadcrumbLink, | |
| BreadcrumbList, | |
| BreadcrumbPage, | |
| BreadcrumbSeparator, | |
| } from "@/components/ui/breadcrumb" | |
| import { | |
| SidebarInset, | |
| SidebarProvider, | |
| SidebarTrigger, | |
| } from "@/components/ui/sidebar" | |
| export default function Page() { | |
| return ( | |
| <SidebarProvider> | |
| <SidebarInset> | |
| <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4"> | |
| <Breadcrumb> | |
| <BreadcrumbList> | |
| <BreadcrumbItem className="hidden md:block"> | |
| <BreadcrumbLink href="#">Build Your Application</BreadcrumbLink> | |
| </BreadcrumbItem> | |
| <BreadcrumbSeparator className="hidden md:block" /> | |
| <BreadcrumbItem> | |
| <BreadcrumbPage>Data Fetching</BreadcrumbPage> | |
| </BreadcrumbItem> | |
| </BreadcrumbList> | |
| </Breadcrumb> | |
| <SidebarTrigger className="-mr-1 ml-auto rotate-180" /> | |
| </header> | |
| <div className="flex flex-1 flex-col gap-4 p-4"> | |
| <div className="grid auto-rows-min gap-4 md:grid-cols-3"> | |
| <div className="aspect-video rounded-xl bg-muted/50" /> | |
| <div className="aspect-video rounded-xl bg-muted/50" /> | |
| <div className="aspect-video rounded-xl bg-muted/50" /> | |
| </div> | |
| <div className="min-h-screen flex-1 rounded-xl bg-muted/50 md:min-h-min" /> | |
| </div> | |
| </SidebarInset> | |
| <AppSidebar side="right" /> | |
| </SidebarProvider> | |
| ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { | |
| Attachment, | |
| AttachmentPreview, | |
| AttachmentRemove, | |
| Attachments, | |
| } from "@/components/ai-elements/attachments"; | |
| import { | |
| PromptInput, | |
| PromptInputActionAddAttachments, | |
| PromptInputActionAddScreenshot, | |
| PromptInputActionMenu, | |
| PromptInputActionMenuContent, | |
| PromptInputActionMenuTrigger, | |
| PromptInputBody, | |
| PromptInputButton, | |
| PromptInputHeader, | |
| type PromptInputMessage, | |
| PromptInputSelect, | |
| PromptInputSelectContent, | |
| PromptInputSelectItem, | |
| PromptInputSelectTrigger, | |
| PromptInputSelectValue, | |
| PromptInputSubmit, | |
| PromptInputTextarea, | |
| PromptInputFooter, | |
| PromptInputTools, | |
| usePromptInputAttachments, | |
| } from "@/components/ai-elements/prompt-input"; | |
| import { GlobeIcon } from "lucide-react"; | |
| import { useState, useEffect } from "react"; | |
| import { useChat } from "@ai-sdk/react"; | |
| import { | |
| Conversation, | |
| ConversationContent, | |
| ConversationScrollButton, | |
| } from "@/components/ai-elements/conversation"; | |
| import { | |
| Message, | |
| MessageContent, | |
| MessageResponse, | |
| } from "@/components/ai-elements/message"; | |
| import { | |
| DefaultChatTransport, | |
| lastAssistantMessageIsCompleteWithToolCalls, | |
| } from "ai"; | |
| // import { getMe } from "@/apis/me.api.ts"; | |
| const PromptInputAttachmentsDisplay = () => { | |
| const attachments = usePromptInputAttachments(); | |
| if (attachments.files.length === 0) { | |
| return null; | |
| } | |
| return ( | |
| <Attachments variant="inline"> | |
| {attachments.files.map((attachment) => ( | |
| <Attachment | |
| data={attachment} | |
| key={attachment.id} | |
| onRemove={() => attachments.remove(attachment.id)} | |
| > | |
| <AttachmentPreview /> | |
| <AttachmentRemove /> | |
| </Attachment> | |
| ))} | |
| </Attachments> | |
| ); | |
| }; | |
| const models = [ | |
| { id: "gpt-4o", name: "GPT-4o" }, | |
| { id: "claude-opus-4-20250514", name: "Claude 4 Opus" }, | |
| ]; | |
| const InputDemo = () => { | |
| const [text, setText] = useState<string>(""); | |
| const [model, setModel] = useState<string>(models[0].id); | |
| const [useWebSearch, setUseWebSearch] = useState<boolean>(false); | |
| const [apiKey, setApiKey] = useState<string>( | |
| localStorage.getItem("apiKey") || "", | |
| ); | |
| useEffect(() => { | |
| // getMe().then((me) => { | |
| // if (!apiKey) { | |
| // localStorage.setItem("apiKey", me.data.userSlug?.[0]); | |
| // setApiKey(me.data.userSlug?.[0] || ""); | |
| // } | |
| // }); | |
| }, []); | |
| // console.log(process.env.NEXT_PUBLIC_BESPOKIBLE_API_URL); | |
| const { messages, status, sendMessage, addToolOutput } = useChat({ | |
| transport: new DefaultChatTransport({ | |
| api: "/api/chat", | |
| headers: { | |
| "X-API-KEY": apiKey, | |
| }, | |
| }), | |
| sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, | |
| async onToolCall({ toolCall }) { | |
| // Check if it's a dynamic tool first for proper type narrowing | |
| if (toolCall.dynamic) { | |
| return; | |
| } | |
| // const cities = ["New York", "Los Angeles", "Chicago", "San Francisco"]; | |
| // No await - avoids potential deadlocks | |
| addToolOutput({ | |
| tool: toolCall.toolName, | |
| toolCallId: toolCall.toolCallId, | |
| output: toolCall.input, | |
| }); | |
| }, | |
| // transport: new DefaultChatTransport({ | |
| // api: process.env.NEXT_PUBLIC_BESPOKIBLE_API_URL as string, // "https://mcp.bespokible.com/chat", | |
| // headers: { | |
| // "X-API-KEY": process.env.NEXT_PUBLIC_BESPOKIBLE_API_KEY as string, | |
| // }, | |
| // }), | |
| }); | |
| useEffect(() => { | |
| document.body.scrollBy(1000, 1000); | |
| }, [messages]); | |
| const handleSubmit = (message: PromptInputMessage) => { | |
| const hasText = Boolean(message.text); | |
| const hasAttachments = Boolean(message.files?.length); | |
| if (!(hasText || hasAttachments)) { | |
| return; | |
| } | |
| sendMessage( | |
| { | |
| text: message.text || "Sent with attachments", | |
| files: message.files, | |
| }, | |
| { | |
| body: { | |
| model: model, | |
| webSearch: useWebSearch, | |
| }, | |
| }, | |
| ); | |
| setText(""); | |
| }; | |
| return ( | |
| <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border min-h-150"> | |
| <div className="flex flex-col h-full"> | |
| <Conversation> | |
| <ConversationContent> | |
| {messages.map((message) => ( | |
| <Message from={message.role} key={message.id}> | |
| <MessageContent> | |
| {message.parts.map((part, i) => { | |
| switch (part.type) { | |
| case "text": | |
| return ( | |
| <MessageResponse key={`${message.id}-${i}`}> | |
| {part.text} | |
| </MessageResponse> | |
| ); | |
| default: | |
| return null; | |
| } | |
| })} | |
| </MessageContent> | |
| </Message> | |
| ))} | |
| </ConversationContent> | |
| <ConversationScrollButton /> | |
| </Conversation> | |
| <PromptInput | |
| onSubmit={handleSubmit} | |
| // className="mt-auto absolute bottom-10" | |
| globalDrop | |
| multiple | |
| > | |
| <PromptInputHeader> | |
| <PromptInputAttachmentsDisplay /> | |
| </PromptInputHeader> | |
| <PromptInputBody> | |
| <PromptInputTextarea | |
| onChange={(e) => setText(e.target.value)} | |
| value={text} | |
| /> | |
| </PromptInputBody> | |
| <PromptInputFooter> | |
| <PromptInputTools> | |
| <PromptInputActionMenu> | |
| <PromptInputActionMenuTrigger /> | |
| <PromptInputActionMenuContent> | |
| <PromptInputActionAddAttachments /> | |
| <PromptInputActionAddScreenshot /> | |
| </PromptInputActionMenuContent> | |
| </PromptInputActionMenu> | |
| {/*<PromptInputButton | |
| onClick={() => setUseWebSearch(!useWebSearch)} | |
| tooltip={{ content: "Search the web", shortcut: "⌘K" }} | |
| variant={useWebSearch ? "default" : "ghost"} | |
| > | |
| <GlobeIcon size={16} /> | |
| <span>Search</span> | |
| </PromptInputButton>*/} | |
| <PromptInputSelect | |
| onValueChange={(value) => { | |
| setModel(value as string); | |
| }} | |
| value={model} | |
| > | |
| <PromptInputSelectTrigger> | |
| <PromptInputSelectValue /> | |
| </PromptInputSelectTrigger> | |
| <PromptInputSelectContent> | |
| {models.map((model) => ( | |
| <PromptInputSelectItem key={model.id} value={model.id}> | |
| {model.name} | |
| </PromptInputSelectItem> | |
| ))} | |
| </PromptInputSelectContent> | |
| </PromptInputSelect> | |
| </PromptInputTools> | |
| <PromptInputSubmit disabled={!text && !status} status={status} /> | |
| </PromptInputFooter> | |
| </PromptInput> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default InputDemo; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { convertToModelMessages, streamText, UIMessage } from "ai"; | |
| import { google } from "@ai-sdk/google"; | |
| import { createMCPClient } from "@ai-sdk/mcp"; | |
| import { headers } from "next/headers"; | |
| // Allow streaming responses up to 30 seconds | |
| // export const maxDuration = 30; | |
| export async function POST(req: Request) { | |
| const { messages }: { messages: UIMessage[] } = await req.json(); | |
| const headersList = await headers(); | |
| const apiKey = headersList.get("X-API-KEY"); | |
| const [artisanMCP, apiMCP] = await Promise.all([ | |
| createMCPClient({ | |
| clientName: "Bespokible Artisan MCP server", | |
| transport: { | |
| type: "http", | |
| url: process.env.BESPOKIBLE_ARTISAN_MCP as string, | |
| headers: { | |
| "X-API-KEY": apiKey, | |
| }, | |
| }, | |
| }), | |
| createMCPClient({ | |
| clientName: "Bespokible OpenAPI MCP server", | |
| transport: { | |
| type: "http", | |
| url: process.env.BESPOKIBLE_API_MCP as string, | |
| headers: { | |
| "X-API-KEY": apiKey, | |
| }, | |
| }, | |
| }), | |
| ]); | |
| const [artisanTools, apiTools] = await Promise.all([artisanMCP.tools(),apiMCP.tools()]); | |
| const result = streamText({ | |
| model: google("gemini-2.5-flash"), | |
| system: `You are a helpful assistant, that passes user queries to the Bespokible MCP server. | |
| - Whatever the agent and tool of the MCP server outputs, you present its summarry to the user | |
| - The bespokible Artisan Server deals soecifically with Artisans and their timesheet entries | |
| - While in genrate, the Bespokbile OpenAPI mcp server can call any route to the bespokible backend to server the user query, | |
| when you can't find a suitable tool in the Artisan MCP server, you can use the OpenAPI MCP server to search & call any route to the backend`, | |
| tools: { | |
| ...artisanTools, | |
| ...apiTools | |
| }, | |
| messages: await convertToModelMessages(messages), | |
| }); | |
| return result.toUIMessageStreamResponse(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "bespokible-ai", | |
| "version": "0.1.0", | |
| "private": true, | |
| "scripts": { | |
| "dev": "next dev", | |
| "build": "next build", | |
| "start": "next start", | |
| "lint": "biome check", | |
| "format": "biome format --write", | |
| "dev:vinext": "vinext dev --port 3001", | |
| "build:vinext": "vinext build", | |
| "start:vinext": "vinext start" | |
| }, | |
| "dependencies": { | |
| "@ai-sdk/google": "^3.0.75", | |
| "@ai-sdk/mcp": "^1.0.42", | |
| "@ai-sdk/openai": "^3.0.64", | |
| "@ai-sdk/react": "^3.0.186", | |
| "@base-ui/react": "^1.4.1", | |
| "@streamdown/cjk": "^1.0.3", | |
| "@streamdown/code": "^1.1.1", | |
| "@streamdown/math": "^1.0.2", | |
| "@streamdown/mermaid": "^1.0.2", | |
| "ai": "^6.0.184", | |
| "class-variance-authority": "^0.7.1", | |
| "clsx": "^2.1.1", | |
| "cmdk": "^1.1.1", | |
| "lucide-react": "^1.16.0", | |
| "nanoid": "^5.1.11", | |
| "next": "16.2.6", | |
| "react": "^19.2.6", | |
| "react-dom": "^19.2.6", | |
| "shadcn": "^4.7.0", | |
| "streamdown": "^2.5.0", | |
| "tailwind-merge": "^3.6.0", | |
| "tw-animate-css": "^1.4.0", | |
| "use-stick-to-bottom": "^1.1.4", | |
| "zod": "^4.4.3" | |
| }, | |
| "devDependencies": { | |
| "@biomejs/biome": "2.2.0", | |
| "@tailwindcss/postcss": "^4", | |
| "@types/node": "^20", | |
| "@types/react": "^19", | |
| "@types/react-dom": "^19", | |
| "@vitejs/plugin-react": "^6.0.2", | |
| "@vitejs/plugin-rsc": "^0.5.26", | |
| "react-server-dom-webpack": "^19.2.6", | |
| "tailwindcss": "^4", | |
| "typescript": "^5", | |
| "vinext": "^0.0.50", | |
| "vite": "^8.0.13" | |
| }, | |
| "ignoreScripts": [ | |
| "sharp", | |
| "unrs-resolver" | |
| ], | |
| "trustedDependencies": [ | |
| "sharp", | |
| "unrs-resolver" | |
| ], | |
| "type": "module" | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { convertToModelMessages, streamText, UIMessage } from "ai"; | |
| import { google } from "@ai-sdk/google"; | |
| import { createMCPClient } from "@ai-sdk/mcp"; | |
| const artisanMCP = await createMCPClient({ | |
| clientName: "Bespokible Artisan MCP server", | |
| transport: { | |
| type: "http", | |
| url: process.env.BESPOKIBLE_ARTISAN_MCP as string, | |
| headers: { | |
| "X-API-KEY": process.env.BESPOKIBLE_API_KEY as string, | |
| }, | |
| }, | |
| }); | |
| const apiMCP = await createMCPClient({ | |
| clientName: "Bespokible OpenAPI MCP server", | |
| transport: { | |
| type: "http", | |
| url: process.env.BESPOKIBLE_API_MCP as string, | |
| headers: { | |
| "X-API-KEY": process.env.BESPOKIBLE_API_KEY as string, | |
| }, | |
| }, | |
| }); | |
| const tools = { ...(await artisanMCP.tools()), ...(await apiMCP.tools()) }; | |
| // Allow streaming responses up to 30 seconds | |
| // export const maxDuration = 30; | |
| export async function POST(req: Request) { | |
| const { messages }: { messages: UIMessage[] } = await req.json(); | |
| const result = streamText({ | |
| model: google("gemini-2.5-flash"), | |
| system: `You are a helpful assistant, that passes user queries to the Bespokible MCP server. | |
| - Whatever the agent and tool of the MCP server outputs, you present its summarry to the user | |
| - The bespokible Artisan Server deals soecifically with Artisans and their timesheet entries | |
| - While in genrate, the Bespokbile OpenAPI mcp server can call any route to the bespokible backend to server the user query, | |
| when you can't find a suitable tool in the Artisan MCP server, you can use the OpenAPI MCP server to search & call any route to the backend`, | |
| tools, | |
| messages: await convertToModelMessages(messages), | |
| }); | |
| return result.toUIMessageStreamResponse(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment