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:
@@ -38,6 +38,7 @@ import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/Cloud
|
||||
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
|
||||
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
|
||||
import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
|
||||
import McpTab from '~/components/@settings/tabs/mcp/McpTab';
|
||||
|
||||
interface ControlPanelProps {
|
||||
open: boolean;
|
||||
@@ -81,6 +82,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
update: 'Check for updates and release notes',
|
||||
'task-manager': 'Monitor system resources and processes',
|
||||
'tab-management': 'Configure visible tabs and their order',
|
||||
mcp: 'Configure MCP (Model Context Protocol) servers',
|
||||
};
|
||||
|
||||
// Beta status for experimental features
|
||||
@@ -335,6 +337,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
return <TaskManagerTab />;
|
||||
case 'service-status':
|
||||
return <ServiceStatusTab />;
|
||||
case 'mcp':
|
||||
return <McpTab />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const TAB_ICONS: Record<TabType, string> = {
|
||||
update: 'i-ph:arrow-clockwise-fill',
|
||||
'task-manager': 'i-ph:chart-line-fill',
|
||||
'tab-management': 'i-ph:squares-four-fill',
|
||||
mcp: 'i-ph:hard-drives-bold',
|
||||
};
|
||||
|
||||
export const TAB_LABELS: Record<TabType, string> = {
|
||||
@@ -32,6 +33,7 @@ export const TAB_LABELS: Record<TabType, string> = {
|
||||
update: 'Updates',
|
||||
'task-manager': 'Task Manager',
|
||||
'tab-management': 'Tab Management',
|
||||
mcp: 'MCP Servers',
|
||||
};
|
||||
|
||||
export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
@@ -49,6 +51,7 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
update: 'Check for updates and release notes',
|
||||
'task-manager': 'Monitor system resources and processes',
|
||||
'tab-management': 'Configure visible tabs and their order',
|
||||
mcp: 'Configure MCP (Model Context Protocol) servers',
|
||||
};
|
||||
|
||||
export const DEFAULT_TAB_CONFIG = [
|
||||
@@ -58,18 +61,19 @@ export const DEFAULT_TAB_CONFIG = [
|
||||
{ id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
|
||||
{ id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
|
||||
{ id: 'connection', visible: true, window: 'user' as const, order: 4 },
|
||||
{ id: 'notifications', visible: true, window: 'user' as const, order: 5 },
|
||||
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
|
||||
{ id: 'connection', visible: true, window: 'user' as const, order: 5 },
|
||||
{ id: 'notifications', visible: true, window: 'user' as const, order: 6 },
|
||||
{ id: 'mcp', visible: true, window: 'user' as const, order: 7 },
|
||||
|
||||
// User Window Tabs (In dropdown, initially hidden)
|
||||
{ id: 'profile', visible: false, window: 'user' as const, order: 7 },
|
||||
{ id: 'settings', visible: false, window: 'user' as const, order: 8 },
|
||||
{ id: 'task-manager', visible: false, window: 'user' as const, order: 9 },
|
||||
{ id: 'service-status', visible: false, window: 'user' as const, order: 10 },
|
||||
{ id: 'profile', visible: false, window: 'user' as const, order: 8 },
|
||||
{ id: 'settings', visible: false, window: 'user' as const, order: 9 },
|
||||
{ id: 'task-manager', visible: false, window: 'user' as const, order: 10 },
|
||||
{ id: 'service-status', visible: false, window: 'user' as const, order: 11 },
|
||||
|
||||
// User Window Tabs (Hidden, controlled by TaskManagerTab)
|
||||
{ id: 'debug', visible: false, window: 'user' as const, order: 11 },
|
||||
{ id: 'update', visible: false, window: 'user' as const, order: 12 },
|
||||
{ id: 'debug', visible: false, window: 'user' as const, order: 12 },
|
||||
{ id: 'update', visible: false, window: 'user' as const, order: 13 },
|
||||
|
||||
// Developer Window Tabs (All visible by default)
|
||||
{ id: 'features', visible: true, window: 'developer' as const, order: 0 },
|
||||
|
||||
@@ -16,7 +16,8 @@ export type TabType =
|
||||
| 'event-logs'
|
||||
| 'update'
|
||||
| 'task-manager'
|
||||
| 'tab-management';
|
||||
| 'tab-management'
|
||||
| 'mcp';
|
||||
|
||||
export type WindowType = 'user' | 'developer';
|
||||
|
||||
@@ -81,6 +82,7 @@ export const TAB_LABELS: Record<TabType, string> = {
|
||||
update: 'Updates',
|
||||
'task-manager': 'Task Manager',
|
||||
'tab-management': 'Tab Management',
|
||||
mcp: 'MCP Servers',
|
||||
};
|
||||
|
||||
export const categoryLabels: Record<SettingCategory, string> = {
|
||||
|
||||
@@ -26,6 +26,7 @@ const TAB_ICONS: Record<TabType, string> = {
|
||||
update: 'i-ph:arrow-clockwise-fill',
|
||||
'task-manager': 'i-ph:chart-line-fill',
|
||||
'tab-management': 'i-ph:squares-four-fill',
|
||||
mcp: 'i-ph:hard-drives-bold',
|
||||
};
|
||||
|
||||
// Define which tabs are default in user mode
|
||||
@@ -37,6 +38,7 @@ const DEFAULT_USER_TABS: TabType[] = [
|
||||
'connection',
|
||||
'notifications',
|
||||
'event-logs',
|
||||
'mcp',
|
||||
];
|
||||
|
||||
// Define which tabs can be added to user mode
|
||||
|
||||
99
app/components/@settings/tabs/mcp/McpServerList.tsx
Normal file
99
app/components/@settings/tabs/mcp/McpServerList.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { MCPServer } from '~/lib/services/mcpService';
|
||||
import McpStatusBadge from '~/components/@settings/tabs/mcp/McpStatusBadge';
|
||||
import McpServerListItem from '~/components/@settings/tabs/mcp/McpServerListItem';
|
||||
|
||||
type McpServerListProps = {
|
||||
serverEntries: [string, MCPServer][];
|
||||
expandedServer: string | null;
|
||||
checkingServers: boolean;
|
||||
onlyShowAvailableServers?: boolean;
|
||||
toggleServerExpanded: (serverName: string) => void;
|
||||
};
|
||||
|
||||
export default function McpServerList({
|
||||
serverEntries,
|
||||
expandedServer,
|
||||
checkingServers,
|
||||
onlyShowAvailableServers = false,
|
||||
toggleServerExpanded,
|
||||
}: McpServerListProps) {
|
||||
if (serverEntries.length === 0) {
|
||||
return <p className="text-sm text-bolt-elements-textSecondary">No MCP servers configured</p>;
|
||||
}
|
||||
|
||||
const filteredEntries = onlyShowAvailableServers
|
||||
? serverEntries.filter(([, s]) => s.status === 'available')
|
||||
: serverEntries;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{filteredEntries.map(([serverName, mcpServer]) => {
|
||||
const isAvailable = mcpServer.status === 'available';
|
||||
const isExpanded = expandedServer === serverName;
|
||||
const serverTools = isAvailable ? Object.entries(mcpServer.tools) : [];
|
||||
|
||||
return (
|
||||
<div key={serverName} className="flex flex-col p-2 rounded-md bg-bolt-elements-background-depth-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div
|
||||
onClick={() => toggleServerExpanded(serverName)}
|
||||
className="flex items-center gap-1.5 text-bolt-elements-textPrimary"
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<div
|
||||
className={`i-ph:${isExpanded ? 'caret-down' : 'caret-right'} w-3 h-3 transition-transform duration-150`}
|
||||
/>
|
||||
<span className="font-medium truncate text-left">{serverName}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 truncate">
|
||||
{mcpServer.config.type === 'sse' || mcpServer.config.type === 'streamable-http' ? (
|
||||
<span className="text-xs text-bolt-elements-textSecondary truncate">{mcpServer.config.url}</span>
|
||||
) : (
|
||||
<span className="text-xs text-bolt-elements-textSecondary truncate">
|
||||
{mcpServer.config.command} {mcpServer.config.args?.join(' ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 flex-shrink-0">
|
||||
{checkingServers ? (
|
||||
<McpStatusBadge status="checking" />
|
||||
) : (
|
||||
<McpStatusBadge status={isAvailable ? 'available' : 'unavailable'} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{!isAvailable && mcpServer.error && (
|
||||
<div className="mt-1.5 ml-6 text-xs text-red-600 dark:text-red-400">Error: {mcpServer.error}</div>
|
||||
)}
|
||||
|
||||
{/* Tool list */}
|
||||
{isExpanded && isAvailable && (
|
||||
<div className="mt-2">
|
||||
<div className="text-bolt-elements-textSecondary text-xs font-medium ml-1 mb-1.5">Available Tools:</div>
|
||||
{serverTools.length === 0 ? (
|
||||
<div className="ml-4 text-xs text-bolt-elements-textSecondary">No tools available</div>
|
||||
) : (
|
||||
<div className="mt-1 space-y-2">
|
||||
{serverTools.map(([toolName, toolSchema]) => (
|
||||
<McpServerListItem
|
||||
key={`${serverName}-${toolName}`}
|
||||
toolName={toolName}
|
||||
toolSchema={toolSchema}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
app/components/@settings/tabs/mcp/McpServerListItem.tsx
Normal file
70
app/components/@settings/tabs/mcp/McpServerListItem.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { Tool } from 'ai';
|
||||
|
||||
type ParameterProperty = {
|
||||
type?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type ToolParameters = {
|
||||
jsonSchema: {
|
||||
properties?: Record<string, ParameterProperty>;
|
||||
required?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
type McpToolProps = {
|
||||
toolName: string;
|
||||
toolSchema: Tool;
|
||||
};
|
||||
|
||||
export default function McpServerListItem({ toolName, toolSchema }: McpToolProps) {
|
||||
if (!toolSchema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parameters = (toolSchema.parameters as ToolParameters)?.jsonSchema.properties || {};
|
||||
const requiredParams = (toolSchema.parameters as ToolParameters)?.jsonSchema.required || [];
|
||||
|
||||
return (
|
||||
<div className="mt-2 ml-4 p-3 rounded-md bg-bolt-elements-background-depth-2 text-xs">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h3 className="text-bolt-elements-textPrimary font-semibold truncate" title={toolName}>
|
||||
{toolName}
|
||||
</h3>
|
||||
|
||||
<p className="text-bolt-elements-textSecondary">{toolSchema.description || 'No description available'}</p>
|
||||
|
||||
{Object.keys(parameters).length > 0 && (
|
||||
<div className="mt-2.5">
|
||||
<h4 className="text-bolt-elements-textSecondary font-semibold mb-1.5">Parameters:</h4>
|
||||
<ul className="ml-1 space-y-2">
|
||||
{Object.entries(parameters).map(([paramName, paramDetails]) => (
|
||||
<li key={paramName} className="break-words">
|
||||
<div className="flex items-start">
|
||||
<span className="font-medium text-bolt-elements-textPrimary">
|
||||
{paramName}
|
||||
{requiredParams.includes(paramName) && (
|
||||
<span className="text-red-600 dark:text-red-400 ml-1">*</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="mx-2 text-bolt-elements-textSecondary">•</span>
|
||||
|
||||
<div className="flex-1">
|
||||
{paramDetails.type && (
|
||||
<span className="text-bolt-elements-textSecondary italic">{paramDetails.type}</span>
|
||||
)}
|
||||
{paramDetails.description && (
|
||||
<div className="mt-0.5 text-bolt-elements-textSecondary">{paramDetails.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
app/components/@settings/tabs/mcp/McpStatusBadge.tsx
Normal file
37
app/components/@settings/tabs/mcp/McpStatusBadge.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export default function McpStatusBadge({ status }: { status: 'checking' | 'available' | 'unavailable' }) {
|
||||
const { styles, label, icon, ariaLabel } = useMemo(() => {
|
||||
const base = 'px-2 py-0.5 rounded-full text-xs font-medium flex items-center gap-1 transition-colors';
|
||||
|
||||
const config = {
|
||||
checking: {
|
||||
styles: `${base} bg-blue-100 text-blue-800 dark:bg-blue-900/80 dark:text-blue-200`,
|
||||
label: 'Checking...',
|
||||
ariaLabel: 'Checking server status',
|
||||
icon: <span className="i-svg-spinners:90-ring-with-bg w-3 h-3 text-current animate-spin" aria-hidden="true" />,
|
||||
},
|
||||
available: {
|
||||
styles: `${base} bg-green-100 text-green-800 dark:bg-green-900/80 dark:text-green-200`,
|
||||
label: 'Available',
|
||||
ariaLabel: 'Server available',
|
||||
icon: <span className="i-ph:check-circle w-3 h-3 text-current" aria-hidden="true" />,
|
||||
},
|
||||
unavailable: {
|
||||
styles: `${base} bg-red-100 text-red-800 dark:bg-red-900/80 dark:text-red-200`,
|
||||
label: 'Unavailable',
|
||||
ariaLabel: 'Server unavailable',
|
||||
icon: <span className="i-ph:warning-circle w-3 h-3 text-current" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
return config[status];
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<span className={styles} role="status" aria-live="polite" aria-label={ariaLabel}>
|
||||
{icon}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
239
app/components/@settings/tabs/mcp/McpTab.tsx
Normal file
239
app/components/@settings/tabs/mcp/McpTab.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import type { MCPConfig } from '~/lib/services/mcpService';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useMCPStore } from '~/lib/stores/mcp';
|
||||
import McpServerList from '~/components/@settings/tabs/mcp/McpServerList';
|
||||
|
||||
const EXAMPLE_MCP_CONFIG: MCPConfig = {
|
||||
mcpServers: {
|
||||
everything: {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-everything'],
|
||||
},
|
||||
deepwiki: {
|
||||
type: 'streamable-http',
|
||||
url: 'https://mcp.deepwiki.com/mcp',
|
||||
},
|
||||
'local-sse': {
|
||||
type: 'sse',
|
||||
url: 'http://localhost:8000/sse',
|
||||
headers: {
|
||||
Authorization: 'Bearer mytoken123',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default function McpTab() {
|
||||
const settings = useMCPStore((state) => state.settings);
|
||||
const isInitialized = useMCPStore((state) => state.isInitialized);
|
||||
const serverTools = useMCPStore((state) => state.serverTools);
|
||||
const initialize = useMCPStore((state) => state.initialize);
|
||||
const updateSettings = useMCPStore((state) => state.updateSettings);
|
||||
const checkServersAvailabilities = useMCPStore((state) => state.checkServersAvailabilities);
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [mcpConfigText, setMCPConfigText] = useState('');
|
||||
const [maxLLMSteps, setMaxLLMSteps] = useState(1);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isCheckingServers, setIsCheckingServers] = useState(false);
|
||||
const [expandedServer, setExpandedServer] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
initialize().catch((err) => {
|
||||
setError(`Failed to initialize MCP settings: ${err instanceof Error ? err.message : String(err)}`);
|
||||
toast.error('Failed to load MCP configuration');
|
||||
});
|
||||
}
|
||||
}, [isInitialized]);
|
||||
|
||||
useEffect(() => {
|
||||
setMCPConfigText(JSON.stringify(settings.mcpConfig, null, 2));
|
||||
setMaxLLMSteps(settings.maxLLMSteps);
|
||||
setError(null);
|
||||
}, [settings]);
|
||||
|
||||
const parsedConfig = useMemo(() => {
|
||||
try {
|
||||
setError(null);
|
||||
return JSON.parse(mcpConfigText) as MCPConfig;
|
||||
} catch (e) {
|
||||
setError(`Invalid JSON format: ${e instanceof Error ? e.message : String(e)}`);
|
||||
return null;
|
||||
}
|
||||
}, [mcpConfigText]);
|
||||
|
||||
const handleMaxLLMCallChange = (value: string) => {
|
||||
setMaxLLMSteps(parseInt(value, 10));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!parsedConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
await updateSettings({
|
||||
mcpConfig: parsedConfig,
|
||||
maxLLMSteps,
|
||||
});
|
||||
toast.success('MCP configuration saved');
|
||||
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to save configuration');
|
||||
toast.error('Failed to save MCP configuration');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadExample = () => {
|
||||
setMCPConfigText(JSON.stringify(EXAMPLE_MCP_CONFIG, null, 2));
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const checkServerAvailability = async () => {
|
||||
if (serverEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingServers(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await checkServersAvailabilities();
|
||||
} catch (e) {
|
||||
setError(`Failed to check server availability: ${e instanceof Error ? e.message : String(e)}`);
|
||||
} finally {
|
||||
setIsCheckingServers(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleServerExpanded = (serverName: string) => {
|
||||
setExpandedServer(expandedServer === serverName ? null : serverName);
|
||||
};
|
||||
|
||||
const serverEntries = useMemo(() => Object.entries(serverTools), [serverTools]);
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<section aria-labelledby="server-status-heading">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h2 className="text-base font-medium text-bolt-elements-textPrimary">MCP Servers Configured</h2>{' '}
|
||||
<button
|
||||
onClick={checkServerAvailability}
|
||||
disabled={isCheckingServers || !parsedConfig || serverEntries.length === 0}
|
||||
className={classNames(
|
||||
'px-3 py-1.5 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-4',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'transition-all duration-200',
|
||||
'flex items-center gap-2',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
{isCheckingServers ? (
|
||||
<div className="i-svg-spinners:90-ring-with-bg w-3 h-3 text-bolt-elements-loader-progress animate-spin" />
|
||||
) : (
|
||||
<div className="i-ph:arrow-counter-clockwise w-3 h-3" />
|
||||
)}
|
||||
Check availability
|
||||
</button>
|
||||
</div>
|
||||
<McpServerList
|
||||
checkingServers={isCheckingServers}
|
||||
expandedServer={expandedServer}
|
||||
serverEntries={serverEntries}
|
||||
toggleServerExpanded={toggleServerExpanded}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="config-section-heading">
|
||||
<h2 className="text-base font-medium text-bolt-elements-textPrimary mb-3">Configuration</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="mcp-config" className="block text-sm text-bolt-elements-textSecondary mb-2">
|
||||
Configuration JSON
|
||||
</label>
|
||||
<textarea
|
||||
id="mcp-config"
|
||||
value={mcpConfigText}
|
||||
onChange={(e) => setMCPConfigText(e.target.value)}
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm font-mono h-72',
|
||||
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
||||
'border',
|
||||
error ? 'border-bolt-elements-icon-error' : 'border-[#E5E5E5] dark:border-[#333333]',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-focus',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>{error && <p className="mt-2 mb-2 text-sm text-bolt-elements-icon-error">{error}</p>}</div>
|
||||
<div>
|
||||
<label htmlFor="max-llm-steps" className="block text-sm text-bolt-elements-textSecondary mb-2">
|
||||
Maximum number of sequential LLM calls (steps)
|
||||
</label>
|
||||
<input
|
||||
id="max-llm-steps"
|
||||
type="number"
|
||||
placeholder="Maximum number of sequential LLM calls"
|
||||
min="1"
|
||||
max="20"
|
||||
value={maxLLMSteps}
|
||||
onChange={(e) => handleMaxLLMCallChange(e.target.value)}
|
||||
className="w-full px-3 py-2 text-bolt-elements-textPrimary text-sm rounded-lg bg-white dark:bg-bolt-elements-background-depth-4 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-bolt-elements-textSecondary">
|
||||
The MCP configuration format is identical to the one used in Claude Desktop.
|
||||
<a
|
||||
href="https://modelcontextprotocol.io/examples"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bolt-elements-link hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
View example servers
|
||||
<div className="i-ph:arrow-square-out w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex flex-wrap justify-between gap-3 mt-6">
|
||||
<button
|
||||
onClick={handleLoadExample}
|
||||
className="px-4 py-2 rounded-lg text-sm border border-bolt-elements-borderColor
|
||||
bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary
|
||||
hover:bg-bolt-elements-background-depth-3"
|
||||
>
|
||||
Load Example
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || !parsedConfig}
|
||||
aria-disabled={isSaving || !parsedConfig}
|
||||
className={classNames(
|
||||
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
||||
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent',
|
||||
'hover:bg-bolt-elements-item-backgroundActive',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<div className="i-ph:floppy-disk w-4 h-4" />
|
||||
{isSaving ? 'Saving...' : 'Save Configuration'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user