diff --git a/examples/chat-ui/pnpm-lock.yaml b/examples/chat-ui/pnpm-lock.yaml index d7e5d41..8e011de 100644 --- a/examples/chat-ui/pnpm-lock.yaml +++ b/examples/chat-ui/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: specifier: ^4.0.14 version: 4.1.6 use-mcp: - specifier: link:.. - version: link:.. + specifier: link:../.. + version: link:../.. workers-ai-provider: specifier: ^0.1.3 version: 0.1.3(react@19.1.0) diff --git a/examples/chat-ui/src/components/ChatApp.tsx b/examples/chat-ui/src/components/ChatApp.tsx index e01aa2d..d07ca35 100644 --- a/examples/chat-ui/src/components/ChatApp.tsx +++ b/examples/chat-ui/src/components/ChatApp.tsx @@ -8,6 +8,7 @@ import { useIndexedDB } from "../hooks/useIndexedDB"; import { type Model } from "../types/models"; import { getSelectedModel, setSelectedModel as saveSelectedModel } from "../utils/modelPreferences"; import { type IDBPDatabase } from "idb"; +import { type Tool } from "use-mcp/react"; // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface ChatAppProps {} @@ -20,6 +21,7 @@ const ChatApp: React.FC = () => { const [sidebarVisible, setSidebarVisible] = useState(true); const [selectedModel, setSelectedModel] = useState(getSelectedModel()); const [apiKeyUpdateTrigger, setApiKeyUpdateTrigger] = useState(0); + const [mcpTools, setMcpTools] = useState([]); const db = useIndexedDB(); const handleApiKeyUpdate = () => { @@ -103,18 +105,19 @@ const ChatApp: React.FC = () => {
+ sidebarVisible={sidebarVisible} + setSidebarVisible={setSidebarVisible} + conversations={conversations} + conversationId={conversationId} + setConversationId={setConversationId} + deleteConversation={deleteConversation} + editConversationTitle={editConversationTitle} + startNewConversation={startNewConversation} + selectedModel={selectedModel} + onModelChange={handleModelChange} + apiKeyUpdateTrigger={apiKeyUpdateTrigger} + onMcpToolsUpdate={setMcpTools} + />
= () => { db={db as IDBPDatabase} selectedModel={selectedModel} onApiKeyUpdate={handleApiKeyUpdate} + mcpTools={mcpTools} />
diff --git a/examples/chat-ui/src/components/ChatSidebar.tsx b/examples/chat-ui/src/components/ChatSidebar.tsx index 0fdef8c..9260969 100644 --- a/examples/chat-ui/src/components/ChatSidebar.tsx +++ b/examples/chat-ui/src/components/ChatSidebar.tsx @@ -10,6 +10,7 @@ import ModelSelector from "./ModelSelector"; import { McpServers } from "./McpServers"; import { type Model } from "../types/models"; import { type Message } from "../types"; +import { type Tool } from "use-mcp/react"; interface Conversation { id?: number; @@ -29,6 +30,7 @@ interface ChatSidebarProps { selectedModel: Model; onModelChange: (model: Model) => void; apiKeyUpdateTrigger: number; + onMcpToolsUpdate: (tools: Tool[]) => void; } const ChatSidebar: React.FC = ({ @@ -43,6 +45,7 @@ const ChatSidebar: React.FC = ({ selectedModel, onModelChange, apiKeyUpdateTrigger, + onMcpToolsUpdate, }) => { const handleConversationClick = (id: number | undefined) => { setConversationId(id); @@ -163,7 +166,7 @@ const ChatSidebar: React.FC = ({ onModelChange={onModelChange} apiKeyUpdateTrigger={apiKeyUpdateTrigger} /> - +
diff --git a/examples/chat-ui/src/components/ConversationThread.tsx b/examples/chat-ui/src/components/ConversationThread.tsx index e533646..e864358 100644 --- a/examples/chat-ui/src/components/ConversationThread.tsx +++ b/examples/chat-ui/src/components/ConversationThread.tsx @@ -5,6 +5,7 @@ import '../styles/github.css' import { type Conversation, type Message } from '../types' import { type Model } from '../types/models' import { type IDBPDatabase } from 'idb' +import { type Tool } from 'use-mcp/react' import ChatMessage from './ChatMessage' import ChatInput from './ChatInput' import { useAutoscroll } from '../hooks/useAutoscroll' @@ -20,6 +21,7 @@ interface ConversationThreadProps { db: IDBPDatabase selectedModel: Model onApiKeyUpdate: () => void + mcpTools: Tool[] } const ConversationThread: React.FC = ({ @@ -30,6 +32,7 @@ const ConversationThread: React.FC = ({ db, selectedModel, onApiKeyUpdate, + mcpTools, }) => { const [input, setInput] = useState('') const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({ @@ -80,6 +83,7 @@ const ConversationThread: React.FC = ({ scrollToBottom, selectedModel, onApiKeyRequired: handleApiKeyRequired, + mcpTools, }) const currentConversation = conversations.find((conv) => conv.id === conversationId) || { messages: [], title: '' } diff --git a/examples/chat-ui/src/hooks/useStreamResponse.ts b/examples/chat-ui/src/hooks/useStreamResponse.ts index 8c39d67..da194fd 100644 --- a/examples/chat-ui/src/hooks/useStreamResponse.ts +++ b/examples/chat-ui/src/hooks/useStreamResponse.ts @@ -1,10 +1,11 @@ import { useState, useRef } from 'react' -import { streamText } from 'ai' +import { streamText, tool, jsonSchema } from 'ai' import { createGroq } from '@ai-sdk/groq' import { createAnthropic } from '@ai-sdk/anthropic' import { type Message, type Conversation } from '../types' import { type Model } from '../types/models' import { getApiKey } from '../utils/apiKeys' +import { type Tool } from 'use-mcp/react' interface UseStreamResponseProps { conversationId?: number @@ -12,6 +13,7 @@ interface UseStreamResponseProps { scrollToBottom: () => void selectedModel: Model onApiKeyRequired: (model: Model) => Promise + mcpTools: Tool[] } export const useStreamResponse = ({ @@ -20,12 +22,52 @@ export const useStreamResponse = ({ scrollToBottom, selectedModel, onApiKeyRequired, + mcpTools, }: UseStreamResponseProps) => { const [isLoading, setIsLoading] = useState(false) const [streamStarted, setStreamStarted] = useState(false) const [controller, setController] = useState(new AbortController()) const aiResponseRef = useRef('') + // Convert MCP tools to the format expected by the 'ai' package + const convertMcpToolsToAiTools = (mcpTools: Tool[]) => { + const aiTools: Record = {} + + console.log(`[useStreamResponse] Converting ${mcpTools.length} MCP tools to AI format`) + + mcpTools.forEach((mcpTool) => { + const schema = mcpTool.inputSchema || { type: 'object', properties: {} } + console.log(`[useStreamResponse] Converting tool: ${mcpTool.name}`, { description: mcpTool.description, schema }) + + aiTools[mcpTool.name] = tool({ + description: mcpTool.description, + parameters: jsonSchema(schema as any), + execute: async (args: unknown, options?: any) => { + console.log(`[useStreamResponse] Tool "${mcpTool.name}" execute called with args:`, args) + console.log(`[useStreamResponse] Tool execute options:`, options) + + try { + // Call the MCP tool using the callTool method attached to the tool + if ('callTool' in mcpTool && typeof mcpTool.callTool === 'function') { + console.log(`[useStreamResponse] Calling MCP tool "${mcpTool.name}"...`) + const result = await mcpTool.callTool(args as Record) + console.log(`[useStreamResponse] Tool "${mcpTool.name}" returned result:`, result) + return result + } else { + throw new Error(`Tool ${mcpTool.name} does not have a callable implementation`) + } + } catch (error) { + console.error(`[useStreamResponse] Error calling MCP tool ${mcpTool.name}:`, error) + throw error + } + }, + }) + }) + + console.log(`[useStreamResponse] Converted tools:`, Object.keys(aiTools)) + return aiTools + } + const getModelInstance = (model: Model, apiKey: string) => { switch (model.provider.id) { case 'groq': { @@ -48,7 +90,10 @@ export const useStreamResponse = ({ const streamResponse = async (messages: Message[]) => { let aiResponse = '' - console.log({ messages }) + console.log(`[useStreamResponse] streamResponse called with ${messages.length} messages`) + console.log(`[useStreamResponse] Messages:`, messages) + console.log(`[useStreamResponse] Current conversationId:`, conversationId) + console.log(`[useStreamResponse] MCP tools available:`, mcpTools.length) // Check if API key is available let apiKey = getApiKey(selectedModel.provider.id) @@ -66,35 +111,88 @@ export const useStreamResponse = ({ try { const modelInstance = getModelInstance(selectedModel, apiKey) + console.log(`[useStreamResponse] Adding empty assistant message to conversation ${conversationId}`) setConversations((prev) => { const updated = [...prev] const conv = updated.find((c) => c.id === conversationId) if (conv) { - console.log('POPPING') + console.log(`[useStreamResponse] Found conversation, adding assistant message. Current message count: ${conv.messages.length}`) conv.messages.push({ role: 'assistant', content: '' }) + console.log(`[useStreamResponse] After adding assistant message, message count: ${conv.messages.length}`) + } else { + console.warn(`[useStreamResponse] Could not find conversation with id: ${conversationId}`) } return updated }) + console.log(`[useStreamResponse] Setting stream started to true`) setStreamStarted(true) + // Convert MCP tools to AI package format + const aiTools = convertMcpToolsToAiTools(mcpTools) + + console.log(`[useStreamResponse] Starting streamText with ${Object.keys(aiTools).length} tools available`) + console.log(`[useStreamResponse] Messages:`, messages.map(m => ({ role: m.role, content: m.content.substring(0, 100) + '...' }))) + const result = await streamText({ model: modelInstance, messages: messages.map((msg) => ({ role: msg.role, content: msg.content, })), + tools: Object.keys(aiTools).length > 0 ? aiTools : undefined, + maxSteps: 5, // Allow up to 5 steps for tool calling + onStepFinish({ text, toolCalls, toolResults, finishReason, usage }) { + console.log(`[useStreamResponse] Step finished:`, { + text: text ? text.substring(0, 100) + '...' : 'no text', + toolCallsCount: toolCalls?.length || 0, + toolResultsCount: toolResults?.length || 0, + finishReason, + usage + }) + if (toolCalls && toolCalls.length > 0) { + console.log(`[useStreamResponse] Tool calls in step:`, toolCalls) + } + if (toolResults && toolResults.length > 0) { + console.log(`[useStreamResponse] Tool results in step:`, toolResults) + } + }, abortSignal: controller.signal, }) + console.log(`[useStreamResponse] streamText result obtained, starting to process stream`) + console.log(`[useStreamResponse] Result object:`, { + hasTextStream: !!result.textStream, + hasToolCalls: !!result.toolCalls, + hasToolResults: !!result.toolResults, + hasFullStream: !!result.fullStream + }) + + // Log tool calls if they exist + if (result.toolCalls) { + console.log(`[useStreamResponse] Tool calls detected:`, result.toolCalls) + } + + // Log tool results if they exist + if (result.toolResults) { + console.log(`[useStreamResponse] Tool results detected:`, result.toolResults) + } + + let chunkCount = 0 for await (const chunk of result.textStream) { try { + chunkCount++ + console.log(`[useStreamResponse] Processing chunk ${chunkCount}:`, JSON.stringify(chunk)) + aiResponse += chunk aiResponseRef.current = aiResponse + console.log(`[useStreamResponse] Updated aiResponse (length: ${aiResponse.length}):`, aiResponse.substring(0, 200) + '...') + //custom extraction of tag const titleMatch = aiResponse.match(/(.*?)<\/chat-title>/) if (titleMatch) { const title = titleMatch[1].trim() + console.log(`[useStreamResponse] Extracted title: ${title}`) setConversations((prev) => { const updated = [...prev] const conv = updated.find((c) => c.id === conversationId) @@ -110,32 +208,44 @@ export const useStreamResponse = ({ const updated = [...prev] const conv = updated.find((c) => c.id === conversationId) if (conv) { + console.log(`[useStreamResponse] Updating conversation message content (length: ${aiResponse.length})`) conv.messages[conv.messages.length - 1].content = aiResponse + } else { + console.warn(`[useStreamResponse] Could not find conversation with id: ${conversationId}`) } return updated }) scrollToBottom() } catch (e) { - console.log('Error in text chunk', e) + console.error('[useStreamResponse] Error in text chunk processing:', e) } } + + console.log(`[useStreamResponse] Finished processing ${chunkCount} chunks. Final response length: ${aiResponse.length}`) } catch (error: unknown) { if (controller.signal.aborted) { - console.log('Stream aborted') + console.log('[useStreamResponse] Stream aborted') } else { - console.error('Error generating response:', error) + console.error('[useStreamResponse] Error generating response:', error) + // Log additional error details + if (error instanceof Error) { + console.error('[useStreamResponse] Error stack:', error.stack) + console.error('[useStreamResponse] Error message:', error.message) + } // Remove the assistant message if there was an error setConversations((prev) => { const updated = [...prev] const conv = updated.find((c) => c.id === conversationId) if (conv && conv.messages[conv.messages.length - 1].role === 'assistant') { + console.log('[useStreamResponse] Removing assistant message due to error') conv.messages.pop() } return updated }) } } finally { + console.log('[useStreamResponse] Cleaning up stream state') setStreamStarted(false) setController(new AbortController()) } diff --git a/wrangler.jsonc b/wrangler.jsonc index c39cf7d..c5985f5 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -20,4 +20,4 @@ "observability": { "enabled": true } -} \ No newline at end of file +}