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:
@@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user