feat(mcp): add Model Context Protocol integration

Add  MCP integration including:
- New MCP settings tab with server configuration
- Tool invocation UI components
- API endpoints for MCP management
- Integration with chat system for tool execution
- Example configurations
This commit is contained in:
Roamin
2025-07-10 17:54:15 +00:00
parent 591c84572d
commit 5de162eec8
26 changed files with 2040 additions and 98 deletions

View File

@@ -1,10 +1,6 @@
/*
* @ts-nocheck
* Preventing TS checks with files presented in the video for a better presentation.
*/
import { useStore } from '@nanostores/react';
import type { Message } from 'ai';
import { useChat } from 'ai/react';
import { useChat } from '@ai-sdk/react';
import { useAnimate } from 'framer-motion';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
@@ -29,6 +25,8 @@ import { filesToArtifacts } from '~/utils/fileUtils';
import { supabaseConnection } from '~/lib/stores/supabase';
import { defaultDesignScheme, type DesignScheme } from '~/types/design-scheme';
import type { ElementInfo } from '~/components/workbench/Inspector';
import type { TextUIPart, FileUIPart, Attachment } from '@ai-sdk/ui-utils';
import { useMCPStore } from '~/lib/stores/mcp';
const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
@@ -148,6 +146,8 @@ export const ChatImpl = memo(
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const [chatMode, setChatMode] = useState<'discuss' | 'build'>('build');
const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(null);
const mcpSettings = useMCPStore((state) => state.settings);
const {
messages,
isLoading,
@@ -161,6 +161,7 @@ export const ChatImpl = memo(
error,
data: chatData,
setData,
addToolResult,
} = useChat({
api: '/api/chat',
body: {
@@ -178,6 +179,7 @@ export const ChatImpl = memo(
anonKey: supabaseConn?.credentials?.anonKey,
},
},
maxLLMSteps: mcpSettings.maxLLMSteps,
},
sendExtraMessageFields: true,
onError: (e) => {
@@ -222,12 +224,7 @@ export const ChatImpl = memo(
runAnimation();
append({
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${prompt}`,
},
] as any, // Type assertion to bypass compiler check
content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${prompt}`,
});
}
}, [model, provider, searchParams]);
@@ -300,6 +297,59 @@ export const ChatImpl = memo(
setChatStarted(true);
};
// Helper function to create message parts array from text and images
const createMessageParts = (text: string, images: string[] = []): Array<TextUIPart | FileUIPart> => {
// Create an array of properly typed message parts
const parts: Array<TextUIPart | FileUIPart> = [
{
type: 'text',
text,
},
];
// Add image parts if any
images.forEach((imageData) => {
// Extract correct MIME type from the data URL
const mimeType = imageData.split(';')[0].split(':')[1] || 'image/jpeg';
// Create file part according to AI SDK format
parts.push({
type: 'file',
mimeType,
data: imageData.replace(/^data:image\/[^;]+;base64,/, ''),
});
});
return parts;
};
// Helper function to convert File[] to Attachment[] for AI SDK
const filesToAttachments = async (files: File[]): Promise<Attachment[] | undefined> => {
if (files.length === 0) {
return undefined;
}
const attachments = await Promise.all(
files.map(
(file) =>
new Promise<Attachment>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve({
name: file.name,
contentType: file.type,
url: reader.result as string,
});
};
reader.readAsDataURL(file);
}),
),
);
return attachments;
};
const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
const messageContent = messageInput || input;
@@ -346,20 +396,14 @@ export const ChatImpl = memo(
if (temResp) {
const { assistantMessage, userMessage } = temResp;
const userMessageText = `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${finalMessageContent}`;
setMessages([
{
id: `1-${new Date().getTime()}`,
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${finalMessageContent}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any,
content: userMessageText,
parts: createMessageParts(userMessageText, imageDataList),
},
{
id: `2-${new Date().getTime()}`,
@@ -373,7 +417,13 @@ export const ChatImpl = memo(
annotations: ['hidden'],
},
]);
reload();
const reloadOptions =
uploadedFiles.length > 0
? { experimental_attachments: await filesToAttachments(uploadedFiles) }
: undefined;
reload(reloadOptions);
setInput('');
Cookies.remove(PROMPT_COOKIE_KEY);
@@ -391,23 +441,19 @@ export const ChatImpl = memo(
}
// If autoSelectTemplate is disabled or template selection failed, proceed with normal message
const userMessageText = `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${finalMessageContent}`;
const attachments = uploadedFiles.length > 0 ? await filesToAttachments(uploadedFiles) : undefined;
setMessages([
{
id: `${new Date().getTime()}`,
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${finalMessageContent}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any,
content: userMessageText,
parts: createMessageParts(userMessageText, imageDataList),
experimental_attachments: attachments,
},
]);
reload();
reload(attachments ? { experimental_attachments: attachments } : undefined);
setFakeLoading(false);
setInput('');
Cookies.remove(PROMPT_COOKIE_KEY);
@@ -432,35 +478,35 @@ export const ChatImpl = memo(
if (modifiedFiles !== undefined) {
const userUpdateArtifact = filesToArtifacts(modifiedFiles, `${Date.now()}`);
append({
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userUpdateArtifact}${finalMessageContent}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any,
});
const messageText = `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${userUpdateArtifact}${finalMessageContent}`;
const attachmentOptions =
uploadedFiles.length > 0 ? { experimental_attachments: await filesToAttachments(uploadedFiles) } : undefined;
append(
{
role: 'user',
content: messageText,
parts: createMessageParts(messageText, imageDataList),
},
attachmentOptions,
);
workbenchStore.resetAllFileModifications();
} else {
append({
role: 'user',
content: [
{
type: 'text',
text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${finalMessageContent}`,
},
...imageDataList.map((imageData) => ({
type: 'image',
image: imageData,
})),
] as any,
});
const messageText = `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${finalMessageContent}`;
const attachmentOptions =
uploadedFiles.length > 0 ? { experimental_attachments: await filesToAttachments(uploadedFiles) } : undefined;
append(
{
role: 'user',
content: messageText,
parts: createMessageParts(messageText, imageDataList),
},
attachmentOptions,
);
}
setInput('');
@@ -579,6 +625,7 @@ export const ChatImpl = memo(
setDesignScheme={setDesignScheme}
selectedElement={selectedElement}
setSelectedElement={setSelectedElement}
addToolResult={addToolResult}
/>
);
},