Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 47 additions & 58 deletions app/routes/chat.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
);
Expand Down Expand Up @@ -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
Expand Down
83 changes: 63 additions & 20 deletions app/services/claude.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Object>} 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) {
Expand All @@ -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);
}
}
}
Expand All @@ -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;
};

Expand All @@ -86,4 +129,4 @@ export function createClaudeService(apiKey = process.env.CLAUDE_API_KEY) {

export default {
createClaudeService
};
};