diff --git a/app/components/@settings/core/ControlPanel.tsx b/app/components/@settings/core/ControlPanel.tsx index 1622146..7292f56 100644 --- a/app/components/@settings/core/ControlPanel.tsx +++ b/app/components/@settings/core/ControlPanel.tsx @@ -25,6 +25,7 @@ import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsT import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab'; import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab'; import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab'; +import McpTab from '~/components/@settings/tabs/mcp/McpTab'; interface ControlPanelProps { open: boolean; @@ -32,7 +33,7 @@ interface ControlPanelProps { } // Beta status for experimental features -const BETA_TABS = new Set(['service-status', 'local-providers']); +const BETA_TABS = new Set(['service-status', 'local-providers', 'mcp']); const BetaLabel = () => (
@@ -139,6 +140,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => { return ; case 'service-status': return ; + case 'mcp': + return ; default: return null; } diff --git a/app/components/@settings/core/constants.ts b/app/components/@settings/core/constants.ts index 5969f3c..8901a8f 100644 --- a/app/components/@settings/core/constants.ts +++ b/app/components/@settings/core/constants.ts @@ -11,6 +11,7 @@ export const TAB_ICONS: Record = { 'service-status': 'i-ph:activity-bold', connection: 'i-ph:wifi-high', 'event-logs': 'i-ph:list-bullets', + mcp: 'i-ph:wrench', }; export const TAB_LABELS: Record = { @@ -24,6 +25,7 @@ export const TAB_LABELS: Record = { 'service-status': 'Service Status', connection: 'Connection', 'event-logs': 'Event Logs', + mcp: 'MCP Servers', }; export const TAB_DESCRIPTIONS: Record = { @@ -37,6 +39,7 @@ export const TAB_DESCRIPTIONS: Record = { 'service-status': 'Monitor cloud LLM service status', connection: 'Check connection status and settings', 'event-logs': 'View system events and logs', + mcp: 'Configure MCP (Model Context Protocol) servers', }; export const DEFAULT_TAB_CONFIG = [ @@ -48,9 +51,10 @@ export const DEFAULT_TAB_CONFIG = [ { 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: 'mcp', visible: true, window: 'user' as const, order: 7 }, + { id: 'profile', visible: true, window: 'user' as const, order: 8 }, + { id: 'service-status', visible: true, window: 'user' as const, order: 9 }, + { id: 'settings', visible: true, window: 'user' as const, order: 10 }, // User Window Tabs (In dropdown, initially hidden) - { id: 'profile', visible: true, window: 'user' as const, order: 7 }, - { id: 'service-status', visible: true, window: 'user' as const, order: 8 }, - { id: 'settings', visible: true, window: 'user' as const, order: 9 }, ]; diff --git a/app/components/@settings/core/types.ts b/app/components/@settings/core/types.ts index d9297cd..d4a518f 100644 --- a/app/components/@settings/core/types.ts +++ b/app/components/@settings/core/types.ts @@ -12,7 +12,8 @@ export type TabType = | 'local-providers' | 'service-status' | 'connection' - | 'event-logs'; + | 'event-logs' + | 'mcp'; export type WindowType = 'user' | 'developer'; @@ -72,6 +73,7 @@ export const TAB_LABELS: Record = { 'service-status': 'Service Status', connection: 'Connections', 'event-logs': 'Event Logs', + mcp: 'MCP Servers', }; export const categoryLabels: Record = { diff --git a/app/components/@settings/tabs/mcp/McpServerList.tsx b/app/components/@settings/tabs/mcp/McpServerList.tsx new file mode 100644 index 0000000..6e15fa9 --- /dev/null +++ b/app/components/@settings/tabs/mcp/McpServerList.tsx @@ -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

No MCP servers configured

; + } + + const filteredEntries = onlyShowAvailableServers + ? serverEntries.filter(([, s]) => s.status === 'available') + : serverEntries; + + return ( +
+ {filteredEntries.map(([serverName, mcpServer]) => { + const isAvailable = mcpServer.status === 'available'; + const isExpanded = expandedServer === serverName; + const serverTools = isAvailable ? Object.entries(mcpServer.tools) : []; + + return ( +
+
+
+
toggleServerExpanded(serverName)} + className="flex items-center gap-1.5 text-bolt-elements-textPrimary" + aria-expanded={isExpanded} + > +
+ {serverName} +
+ +
+ {mcpServer.config.type === 'sse' || mcpServer.config.type === 'streamable-http' ? ( + {mcpServer.config.url} + ) : ( + + {mcpServer.config.command} {mcpServer.config.args?.join(' ')} + + )} +
+
+ +
+ {checkingServers ? ( + + ) : ( + + )} +
+
+ + {/* Error message */} + {!isAvailable && mcpServer.error && ( +
Error: {mcpServer.error}
+ )} + + {/* Tool list */} + {isExpanded && isAvailable && ( +
+
Available Tools:
+ {serverTools.length === 0 ? ( +
No tools available
+ ) : ( +
+ {serverTools.map(([toolName, toolSchema]) => ( + + ))} +
+ )} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/app/components/@settings/tabs/mcp/McpServerListItem.tsx b/app/components/@settings/tabs/mcp/McpServerListItem.tsx new file mode 100644 index 0000000..7013dde --- /dev/null +++ b/app/components/@settings/tabs/mcp/McpServerListItem.tsx @@ -0,0 +1,70 @@ +import type { Tool } from 'ai'; + +type ParameterProperty = { + type?: string; + description?: string; +}; + +type ToolParameters = { + jsonSchema: { + properties?: Record; + 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 ( +
+
+

+ {toolName} +

+ +

{toolSchema.description || 'No description available'}

+ + {Object.keys(parameters).length > 0 && ( +
+

Parameters:

+
    + {Object.entries(parameters).map(([paramName, paramDetails]) => ( +
  • +
    + + {paramName} + {requiredParams.includes(paramName) && ( + * + )} + + + + +
    + {paramDetails.type && ( + {paramDetails.type} + )} + {paramDetails.description && ( +
    {paramDetails.description}
    + )} +
    +
    +
  • + ))} +
+
+ )} +
+
+ ); +} diff --git a/app/components/@settings/tabs/mcp/McpStatusBadge.tsx b/app/components/@settings/tabs/mcp/McpStatusBadge.tsx new file mode 100644 index 0000000..3cbbb1f --- /dev/null +++ b/app/components/@settings/tabs/mcp/McpStatusBadge.tsx @@ -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: