diff --git a/app/routes/chat.jsx b/app/routes/chat.jsx index 26a5be42..e1a2a2c5 100644 --- a/app/routes/chat.jsx +++ b/app/routes/chat.jsx @@ -3,8 +3,7 @@ * Handles chat interactions with Claude API and tools */ import { json } from "@remix-run/node"; -import MCPClient from "../mcp-client"; -import { saveMessage, getConversationHistory, storeCustomerAccountUrl, getCustomerAccountUrl } from "../db.server"; +import { saveMessage, getConversationHistory, storeCustomerAccountUrl, getCustomerAccountUrl, getCustomerToken } from "../db.server"; import AppConfig from "../services/config.server"; import { createSseStream } from "../services/streaming.server"; import { createClaudeService } from "../services/claude.server"; @@ -128,37 +127,19 @@ async function handleChatSession({ stream }) { // Initialize services - const claudeService = createClaudeService(); - const toolService = createToolService(); - - // Initialize MCP client - const shopId = request.headers.get("X-Shopify-Shop-Id"); const shopDomain = request.headers.get("Origin"); + const shopId = request.headers.get("X-Shopify-Shop-Id"); const customerMcpEndpoint = await getCustomerMcpEndpoint(shopDomain, conversationId); - const mcpClient = new MCPClient( - shopDomain, - conversationId, - shopId, - customerMcpEndpoint - ); + const storefrontMcpEndpoint = getStorefrontMcpEndpoint(shopDomain); + const customerAccessToken = await getCustomerAccessToken(conversationId); + + const claudeService = createClaudeService(); + const toolService = createToolService(); try { // Send conversation ID to client stream.sendMessage({ type: 'id', conversation_id: conversationId }); - // Connect to MCP servers and get available tools - let storefrontMcpTools = [], customerMcpTools = []; - - try { - storefrontMcpTools = await mcpClient.connectToStorefrontServer(); - customerMcpTools = await mcpClient.connectToCustomerServer(); - - console.log(`Connected to MCP with ${storefrontMcpTools.length} tools`); - console.log(`Connected to customer MCP with ${customerMcpTools.length} tools`); - } catch (error) { - console.warn('Failed to connect to MCP servers, continuing without tools:', error.message); - } - // Prepare conversation state let conversationHistory = []; let productsToDisplay = []; @@ -186,12 +167,16 @@ async function handleChatSession({ // Execute the conversation stream let finalMessage = { role: 'user', content: userMessage }; - while (finalMessage.stop_reason !== "end_turn") { + while (finalMessage.stop_reason !== "end_turn" && finalMessage.stop_reason !== "auth_required") { finalMessage = await claudeService.streamConversation( { messages: conversationHistory, promptType, - tools: mcpClient.tools + customerMcpEndpoint, + storefrontMcpEndpoint, + customerAccessToken, + shopId, + conversationId }, { // Handle text chunks @@ -218,38 +203,18 @@ async function handleChatSession({ stream.sendMessage({ type: 'message_complete' }); }, - // Handle tool use requests - onToolUse: async (content) => { - const toolName = content.name; - const toolArgs = content.input; - const toolUseId = content.id; - - // Call the tool - const toolUseResponse = await mcpClient.callTool(toolName, toolArgs); - - // Handle tool response based on success/error - if (toolUseResponse.error) { - await toolService.handleToolError( - toolUseResponse, - toolName, - toolUseId, - conversationHistory, - stream.sendMessage, - conversationId - ); - } else { - await toolService.handleToolSuccess( - toolUseResponse, - toolName, - toolUseId, - conversationHistory, - productsToDisplay, - conversationId - ); + // Handle content blocks + onContentBlock: (contentBlock) => { + if (contentBlock.type === 'text') { + stream.sendMessage({ type: 'new_message' }); } + }, - // Signal new message to client - stream.sendMessage({ type: 'new_message' }); + // Handle tool result content blocks + onToolResult: (contentBlock) => { + // Parse products from tool result and add to productsToDisplay + const productsSearchResults = toolService.processProductSearchResult(contentBlock); + productsToDisplay.push(...productsSearchResults); } } ); @@ -315,6 +280,30 @@ async function getCustomerMcpEndpoint(shopDomain, conversationId) { } } +/** + * Get the storefront MCP endpoint for a shop + * @param {string} shopDomain - The shop domain + * @returns {string} The storefront MCP endpoint + */ +function getStorefrontMcpEndpoint(shopDomain) { + return `${shopDomain}/api/mcp`; +} + +/** + * Get the customer access token for a conversation + * @param {string} conversationId - The conversation ID + * @returns {string|null} The customer access token or null if not found + */ +async function getCustomerAccessToken(conversationId) { + const customerAccessToken = await getCustomerToken(conversationId); + if (customerAccessToken) { + return customerAccessToken.accessToken; + } + + return null; +} + + /** * Gets CORS headers for the response * @param {Request} request - The request object diff --git a/app/services/claude.server.js b/app/services/claude.server.js index a93953d0..6f7afd42 100644 --- a/app/services/claude.server.js +++ b/app/services/claude.server.js @@ -5,7 +5,7 @@ import { Anthropic } from "@anthropic-ai/sdk"; import AppConfig from "./config.server"; import systemPrompts from "../prompts/prompts.json"; - +import { generateAuthUrl } from "../auth.server"; /** * Creates a Claude service instance * @param {string} apiKey - Claude API key @@ -20,29 +20,68 @@ export function createClaudeService(apiKey = process.env.CLAUDE_API_KEY) { * @param {Object} params - Stream parameters * @param {Array} params.messages - Conversation history * @param {string} params.promptType - The type of system prompt to use - * @param {Array} params.tools - Available tools for Claude + * @param {string} params.customerMcpEndpoint - The customer MCP endpoint + * @param {string} params.storefrontMcpEndpoint - The storefront MCP endpoint + * @param {string} params.customerAccessToken - The customer access token + * @param {string} params.shopId - The shop ID + * @param {string} params.conversationId - The conversation ID * @param {Object} streamHandlers - Stream event handlers * @param {Function} streamHandlers.onText - Handles text chunks * @param {Function} streamHandlers.onMessage - Handles complete messages * @param {Function} streamHandlers.onToolUse - Handles tool use requests * @returns {Promise} The final message */ - const streamConversation = async ({ - messages, - promptType = AppConfig.api.defaultPromptType, - tools + const streamConversation = async ({ + messages, + promptType = AppConfig.api.defaultPromptType, + customerMcpEndpoint, + storefrontMcpEndpoint, + customerAccessToken, + shopId, + conversationId }, streamHandlers) => { // Get system prompt from configuration or use default const systemInstruction = getSystemPrompt(promptType); + if (!customerAccessToken) { + const authResponse = await generateAuthUrl(conversationId, shopId); + const authRequiredMessage = { + role: "assistant", + content: `You need to authorize the app to access your customer data. [Click here to authorize](${authResponse.url})`, + stop_reason: "auth_required" + }; + streamHandlers.onText(authRequiredMessage.content); + streamHandlers.onMessage(authRequiredMessage); + return authRequiredMessage; + } + // Create stream - const stream = await anthropic.messages.stream({ - model: AppConfig.api.defaultModel, - max_tokens: AppConfig.api.maxTokens, - system: systemInstruction, - messages, - tools: tools && tools.length > 0 ? tools : undefined - }); + const stream = await anthropic.beta.messages.stream( + { + model: AppConfig.api.defaultModel, + max_tokens: AppConfig.api.maxTokens, + system: systemInstruction, + messages, + mcp_servers: [ + { + type: "url", + name: "storefront-mcp-server", + url: storefrontMcpEndpoint + }, + { + type: "url", + name: "customer-mcp-server", + url: customerMcpEndpoint, + authorization_token: customerAccessToken + } + ], + }, + { + headers: { + 'anthropic-beta': 'mcp-client-2025-04-04', + }, + }, + ); // Set up event handlers if (streamHandlers.onText) { @@ -53,14 +92,18 @@ export function createClaudeService(apiKey = process.env.CLAUDE_API_KEY) { stream.on('message', streamHandlers.onMessage); } + if (streamHandlers.onContentBlock) { + stream.on('contentBlock', streamHandlers.onContentBlock); + } + // Wait for final message const finalMessage = await stream.finalMessage(); - - // Process tool use requests - if (streamHandlers.onToolUse && finalMessage.content) { + + // Process tool use results + if (streamHandlers.onToolResult && finalMessage.content) { for (const content of finalMessage.content) { - if (content.type === "tool_use") { - await streamHandlers.onToolUse(content); + if (content.type === "mcp_tool_result") { + await streamHandlers.onToolResult(content); } } } @@ -74,7 +117,7 @@ export function createClaudeService(apiKey = process.env.CLAUDE_API_KEY) { * @returns {string} The system prompt content */ const getSystemPrompt = (promptType) => { - return systemPrompts.systemPrompts[promptType]?.content || + return systemPrompts.systemPrompts[promptType]?.content || systemPrompts.systemPrompts[AppConfig.api.defaultPromptType].content; }; @@ -86,4 +129,4 @@ export function createClaudeService(apiKey = process.env.CLAUDE_API_KEY) { export default { createClaudeService -}; \ No newline at end of file +};