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
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
"turndown": "^7.1.3",
"unist-util-visit": "^5.0.0",
"yt-transcript": "^0.0.2",
"zustand": "^4.5.0"
"zustand": "^4.5.0",
"@modelcontextprotocol/sdk": "^1.11.4"
},
"devDependencies": {
"@plasmohq/prettier-plugin-sort-imports": "4.0.1",
Expand Down
18 changes: 18 additions & 0 deletions src/assets/locale/en/mcpserver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"mcpSettings": {
"addServer": "Add MCP Server",
"editServer": "Edit MCP Server",
"testConnection": "Test Connection",
"testSuccess": "Connection successful. Found {{count}} tool(s).",
"testSuccess_plural": "Connection successful. Found {{count}} tools.",
"testFailure": "Connection failed. Please check the URL and API key.",
"form": {
"name": "Name",
"nameRequired": "Please enter a server name",
"url": "URL",
"urlRequired": "Please enter a valid URL",
"urlPlaceholder": "http://localhost:8000",
"apiKey": "API Key (Optional)"
}
}
}
13 changes: 13 additions & 0 deletions src/assets/locale/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@
"someError": "Something went wrong. Please try again later"
}
},
"mcpSettings": {
"title": "Manage Tools",
"addBtn": "Add New Tools",
"noServers": "No MCP servers configured.",
"deleteConfirm": "Are you sure you want to delete this server?",
"toolsExposed": "Tools:",
"table": {
"status": "Status",
"name": "Name",
"url": "URL",
"actions": "Actions"
}
},
"managePrompts": {
"title": "Manage Prompts",
"addBtn": "Add New Prompt",
Expand Down
26 changes: 24 additions & 2 deletions src/components/Common/Playground/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export const PlaygroundMessage = (props: Props) => {
return (
<Collapse
key={i}
className="border-none text-gray-500 dark:text-gray-400 !mb-3 "
className="border-none text-gray-500 dark:text-gray-400 !mb-3"
defaultActiveKey={
props?.openReasoning ? "reasoning" : undefined
}
Expand All @@ -225,7 +225,29 @@ export const PlaygroundMessage = (props: Props) => {
})
),
children: <Markdown message={e.content} />
}
},
]}
/>
)
} else if (e.type === 'tool_run') {
return (
<Collapse
key={`tool-${i}`}
className="border-none text-gray-500 dark:text-gray-400 !mb-3"
items={[
{
key: "tool_run",
label: e.tool_running ? (
<div className="flex items-center gap-2">
<span className="italic shimmer-text">
Executing...
</span>
</div>
) : (
`Executed for ${humanizeMilliseconds(e.duration || 0)}`
),
children: <Markdown message={e.content} />,
},
]}
/>
)
Expand Down
15 changes: 11 additions & 4 deletions src/components/Layouts/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { useState, useCallback } from "react"

import { Sidebar } from "../Option/Sidebar"
import { Drawer, Tooltip } from "antd"
Expand Down Expand Up @@ -34,11 +34,18 @@ export default function OptionLayout({
temporaryChat,
setSelectedSystemPrompt,
setContextFiles,
useOCR
useOCR,
history // <-- Destructure history here as it's used in the Sidebar props
} = useMessageOption()
const queryClient = useQueryClient()
const { setSystemPrompt } = useStoreChatModelSettings()

// Create a stable function for onClose using useCallback.
// This prevents a new function from being created on every render.
const handleCloseSidebar = useCallback(() => {
setSidebarOpen(false)
}, [])

return (
<div className="flex h-full w-full">
<main className="relative h-dvh w-full">
Expand Down Expand Up @@ -93,11 +100,11 @@ export default function OptionLayout({
}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
onClose={handleCloseSidebar}
open={sidebarOpen}>
<Sidebar
isOpen={sidebarOpen}
onClose={() => setSidebarOpen(false)}
onClose={handleCloseSidebar}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
Expand Down
9 changes: 8 additions & 1 deletion src/components/Layouts/SettingsOptionLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
InfoIcon,
CombineIcon,
ChromeIcon,
CpuIcon
CpuIcon,
Wrench
} from "lucide-react"
import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom"
Expand Down Expand Up @@ -103,6 +104,12 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
current={location.pathname}
icon={BrainCircuitIcon}
/>
<LinkComponent
href="/settings/mcp"
name={t("mcpSettings.title")}
icon={Wrench}
current={location.pathname}
/>
<LinkComponent
href="/settings/knowledge"
name={
Expand Down
117 changes: 117 additions & 0 deletions src/components/Option/Settings/mcp-settings-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal, Form, Input, message } from 'antd';
import { McpServerConfig } from '@/db/dexie/types';
import { McpClient } from '@/mcp/McpClient';

interface McpServerModalProps {
open: boolean;
onClose: () => void;
onSave: (server: McpServerConfig) => void;
server?: McpServerConfig | null;
}

const SpinnerIcon = () => (
<svg className="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
);

export const McpServerModal: React.FC<McpServerModalProps> = ({ open, onClose, onSave, server }) => {
const { t } = useTranslation(['mcpserver', 'settings', 'common']);
const [form] = Form.useForm();
const [isTesting, setIsTesting] = useState(false);
const [isConnectionSuccessful, setIsConnectionSuccessful] = useState(false);

// Define button styles here for consistency and reusability
const primaryButtonClasses = "inline-flex items-center justify-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50";

const secondaryButtonClasses = "inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600 disabled:opacity-50";


useEffect(() => {
if (server) {
form.setFieldsValue(server);
} else {
form.resetFields();
}
setIsConnectionSuccessful(false);
}, [server, open]);

const handleTestConnection = async () => {
try {
const values = await form.validateFields();
setIsTesting(true);
const client = new McpClient(values as McpServerConfig);
try {
const tools = await client.connect();
message.success(t('mcpSettings.testSuccess', { count: tools.length }));
setIsConnectionSuccessful(true);
} finally {
await client.cleanup();
}
} catch (error) {
message.error(t('mcpSettings.testFailure', 'Connection failed. Please check the URL and API key.'));
setIsConnectionSuccessful(false);
} finally {
setIsTesting(false);
}
};

const handleSave = async () => {
try {
const values = await form.validateFields();
onSave({ ...server, ...values });
onClose();
} catch (error) {
console.error('Failed to save server:', error);
}
};

return (
<Modal
title={server ? t('mcpSettings.editServer', 'Edit MCP Server') : t('mcpSettings.addServer', 'Add MCP Server')}
open={open}
onCancel={onClose}
// Set footer to null to use our own custom footer
footer={null}
>
<Form form={form} layout="vertical" name="mcp_server_form">
<Form.Item
name="name"
label={t('mcpSettings.form.name', 'Name')}
rules={[{ required: true, message: t('mcpSettings.form.nameRequired', 'Please enter a server name') }]}
>
<Input />
</Form.Item>
<Form.Item
name="url"
label={t('mcpSettings.form.url', 'URL')}
rules={[{ required: true, type: 'url', message: t('mcpSettings.form.urlRequired', 'Please enter a valid URL') }]}
>
<Input placeholder="http://localhost:8000" />
</Form.Item>
<Form.Item
name="apiKey"
label={t('mcpSettings.form.apiKey', 'API Key (Optional)')}
>
<Input.Password />
</Form.Item>
</Form>

<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button type="button" onClick={onClose} className={secondaryButtonClasses}>
{t('common.cancel', 'Cancel')}
</button>
<button type="button" onClick={handleTestConnection} className={secondaryButtonClasses} disabled={isTesting}>
{isTesting && <SpinnerIcon />}
{t('mcpSettings.testConnection', 'Test Connection')}
</button>
<button type="submit" onClick={handleSave} className={primaryButtonClasses} disabled={!isConnectionSuccessful && !server}>
{t('common.save', 'Save')}
</button>
</div>
</Modal>
);
};
Loading