335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
getErrorMessage,
|
|
useGetCoreConfigQuery,
|
|
useUpdateCoreConfigMutation,
|
|
} from "@/lib/store";
|
|
import { CoreConfig, DefaultCoreConfig } from "@/lib/types/config";
|
|
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
export default function MCPView() {
|
|
const hasSettingsUpdateAccess = useRbac(
|
|
RbacResource.Settings,
|
|
RbacOperation.Update,
|
|
);
|
|
const { data: bifrostConfig } = useGetCoreConfigQuery({ fromDB: true });
|
|
const config = bifrostConfig?.client_config;
|
|
const [updateCoreConfig, { isLoading }] = useUpdateCoreConfigMutation();
|
|
const [localConfig, setLocalConfig] = useState<CoreConfig>(DefaultCoreConfig);
|
|
|
|
const [localValues, setLocalValues] = useState<{
|
|
mcp_agent_depth: string;
|
|
mcp_tool_execution_timeout: string;
|
|
mcp_code_mode_binding_level: string;
|
|
mcp_tool_sync_interval: string;
|
|
}>({
|
|
mcp_agent_depth: "10",
|
|
mcp_tool_execution_timeout: "30",
|
|
mcp_code_mode_binding_level: "server",
|
|
mcp_tool_sync_interval: "10",
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (bifrostConfig && config) {
|
|
setLocalConfig(config);
|
|
setLocalValues({
|
|
mcp_agent_depth: config?.mcp_agent_depth?.toString() || "10",
|
|
mcp_tool_execution_timeout:
|
|
config?.mcp_tool_execution_timeout?.toString() || "30",
|
|
mcp_code_mode_binding_level:
|
|
config?.mcp_code_mode_binding_level || "server",
|
|
mcp_tool_sync_interval:
|
|
config?.mcp_tool_sync_interval?.toString() || "10",
|
|
});
|
|
}
|
|
}, [config, bifrostConfig]);
|
|
|
|
const hasChanges = useMemo(() => {
|
|
if (!config) return false;
|
|
return (
|
|
localConfig.mcp_agent_depth !== config.mcp_agent_depth ||
|
|
localConfig.mcp_tool_execution_timeout !==
|
|
config.mcp_tool_execution_timeout ||
|
|
localConfig.mcp_code_mode_binding_level !==
|
|
(config.mcp_code_mode_binding_level || "server") ||
|
|
localConfig.mcp_tool_sync_interval !==
|
|
(config.mcp_tool_sync_interval ?? 10) ||
|
|
localConfig.mcp_disable_auto_tool_inject !==
|
|
(config.mcp_disable_auto_tool_inject ?? false)
|
|
);
|
|
}, [config, localConfig]);
|
|
|
|
const handleAgentDepthChange = useCallback((value: string) => {
|
|
setLocalValues((prev) => ({ ...prev, mcp_agent_depth: value }));
|
|
const numValue = Number.parseInt(value);
|
|
if (!isNaN(numValue) && numValue > 0) {
|
|
setLocalConfig((prev) => ({ ...prev, mcp_agent_depth: numValue }));
|
|
}
|
|
}, []);
|
|
|
|
const handleToolExecutionTimeoutChange = useCallback((value: string) => {
|
|
setLocalValues((prev) => ({ ...prev, mcp_tool_execution_timeout: value }));
|
|
const numValue = Number.parseInt(value);
|
|
if (!isNaN(numValue) && numValue > 0) {
|
|
setLocalConfig((prev) => ({
|
|
...prev,
|
|
mcp_tool_execution_timeout: numValue,
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
const handleCodeModeBindingLevelChange = useCallback((value: string) => {
|
|
setLocalValues((prev) => ({ ...prev, mcp_code_mode_binding_level: value }));
|
|
if (value === "server" || value === "tool") {
|
|
setLocalConfig((prev) => ({
|
|
...prev,
|
|
mcp_code_mode_binding_level: value,
|
|
}));
|
|
}
|
|
}, []);
|
|
|
|
const handleToolSyncIntervalChange = useCallback((value: string) => {
|
|
setLocalValues((prev) => ({ ...prev, mcp_tool_sync_interval: value }));
|
|
const numValue = Number.parseInt(value);
|
|
if (!isNaN(numValue) && numValue >= 0) {
|
|
setLocalConfig((prev) => ({ ...prev, mcp_tool_sync_interval: numValue }));
|
|
}
|
|
}, []);
|
|
|
|
const handleDisableAutoToolInjectChange = useCallback((checked: boolean) => {
|
|
setLocalConfig((prev) => ({
|
|
...prev,
|
|
mcp_disable_auto_tool_inject: checked,
|
|
}));
|
|
}, []);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
try {
|
|
const agentDepth = Number.parseInt(localValues.mcp_agent_depth);
|
|
const toolTimeout = Number.parseInt(
|
|
localValues.mcp_tool_execution_timeout,
|
|
);
|
|
|
|
if (isNaN(agentDepth) || agentDepth <= 0) {
|
|
toast.error("Max agent depth must be a positive number.");
|
|
return;
|
|
}
|
|
|
|
if (isNaN(toolTimeout) || toolTimeout <= 0) {
|
|
toast.error("Tool execution timeout must be a positive number.");
|
|
return;
|
|
}
|
|
|
|
if (!bifrostConfig) {
|
|
toast.error("Configuration not loaded. Please refresh and try again.");
|
|
return;
|
|
}
|
|
await updateCoreConfig({
|
|
...bifrostConfig,
|
|
client_config: localConfig,
|
|
}).unwrap();
|
|
toast.success("MCP settings updated successfully.");
|
|
} catch (error) {
|
|
toast.error(getErrorMessage(error));
|
|
}
|
|
}, [bifrostConfig, localConfig, localValues, updateCoreConfig]);
|
|
|
|
return (
|
|
<div
|
|
className="mx-auto w-full max-w-7xl space-y-4"
|
|
data-testid="mcp-settings-view"
|
|
>
|
|
<div>
|
|
<h2 className="text-lg font-semibold tracking-tight">MCP Settings</h2>
|
|
<p className="text-muted-foreground text-sm">
|
|
Configure MCP (Model Context Protocol) agent and tool settings.
|
|
</p>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{/* Max Agent Depth */}
|
|
<div className="flex items-center justify-between space-x-2 rounded-sm border p-4">
|
|
<div className="space-y-0.5">
|
|
<label htmlFor="mcp-agent-depth" className="text-sm font-medium">
|
|
Max Agent Depth
|
|
</label>
|
|
<p className="text-muted-foreground text-sm">
|
|
Maximum depth for MCP agent execution.
|
|
</p>
|
|
</div>
|
|
<Input
|
|
id="mcp-agent-depth"
|
|
data-testid="mcp-agent-depth-input"
|
|
type="number"
|
|
className="w-24"
|
|
value={localValues.mcp_agent_depth}
|
|
onChange={(e) => handleAgentDepthChange(e.target.value)}
|
|
min="1"
|
|
/>
|
|
</div>
|
|
|
|
{/* Tool Execution Timeout */}
|
|
<div className="flex items-center justify-between space-x-2 rounded-sm border p-4">
|
|
<div className="space-y-0.5">
|
|
<label
|
|
htmlFor="mcp-tool-execution-timeout"
|
|
className="text-sm font-medium"
|
|
>
|
|
Tool Execution Timeout (seconds)
|
|
</label>
|
|
<p className="text-muted-foreground text-sm">
|
|
Maximum time in seconds for tool execution.
|
|
</p>
|
|
</div>
|
|
<Input
|
|
id="mcp-tool-execution-timeout"
|
|
data-testid="mcp-tool-timeout-input"
|
|
type="number"
|
|
className="w-24"
|
|
value={localValues.mcp_tool_execution_timeout}
|
|
onChange={(e) => handleToolExecutionTimeoutChange(e.target.value)}
|
|
min="1"
|
|
/>
|
|
</div>
|
|
|
|
{/* Tool Sync Interval */}
|
|
<div className="flex items-center justify-between space-x-2 rounded-sm border p-4">
|
|
<div className="space-y-0.5">
|
|
<label
|
|
htmlFor="mcp-tool-sync-interval"
|
|
className="text-sm font-medium"
|
|
>
|
|
Tool Sync Interval (minutes)
|
|
</label>
|
|
<p className="text-muted-foreground text-sm">
|
|
How often to refresh tool lists from MCP servers. Set to 0 to
|
|
disable.
|
|
</p>
|
|
</div>
|
|
<Input
|
|
id="mcp-tool-sync-interval"
|
|
data-testid="mcp-tool-sync-interval-input"
|
|
type="number"
|
|
className="w-24"
|
|
value={localValues.mcp_tool_sync_interval}
|
|
onChange={(e) => handleToolSyncIntervalChange(e.target.value)}
|
|
min="0"
|
|
/>
|
|
</div>
|
|
|
|
{/* Disable Auto Tool Injection */}
|
|
<div className="flex items-center justify-between space-x-2 rounded-sm border p-4">
|
|
<div className="space-y-0.5">
|
|
<label
|
|
htmlFor="mcp-disable-auto-tool-inject"
|
|
className="text-sm font-medium"
|
|
>
|
|
Disable Auto Tool Injection
|
|
</label>
|
|
<p className="text-muted-foreground text-sm">
|
|
When enabled, MCP tools are not automatically included in every
|
|
request. Tools are only injected when explicitly specified via
|
|
request headers (
|
|
<code className="text-xs">x-bf-mcp-include-tools</code>) and still
|
|
must be allowed by the virtual key MCP configuration.
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="mcp-disable-auto-tool-inject"
|
|
checked={localConfig.mcp_disable_auto_tool_inject ?? false}
|
|
onCheckedChange={handleDisableAutoToolInjectChange}
|
|
disabled={!hasSettingsUpdateAccess}
|
|
data-testid="mcp-disable-auto-tool-inject-switch"
|
|
/>
|
|
</div>
|
|
|
|
{/* Code Mode Binding Level */}
|
|
<div className="space-y-4 rounded-sm border p-4">
|
|
<div className="space-y-0.5">
|
|
<label htmlFor="mcp-binding-level" className="text-sm font-medium">
|
|
Code Mode Binding Level
|
|
</label>
|
|
<p className="text-muted-foreground text-sm">
|
|
How tools are exposed in the VFS: server-level (all tools per
|
|
server) or tool-level (individual tools).
|
|
</p>
|
|
</div>
|
|
<Select
|
|
value={localValues.mcp_code_mode_binding_level}
|
|
onValueChange={handleCodeModeBindingLevelChange}
|
|
>
|
|
<SelectTrigger
|
|
id="mcp-binding-level"
|
|
data-testid="mcp-binding-level"
|
|
className="w-56"
|
|
>
|
|
<SelectValue placeholder="Select binding level" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="server">Server-Level</SelectItem>
|
|
<SelectItem value="tool">Tool-Level</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* Visual Example */}
|
|
<div className="mt-6 space-y-2">
|
|
<p className="text-foreground text-xs font-semibold tracking-wide uppercase">
|
|
VFS Structure:
|
|
</p>
|
|
|
|
{localValues.mcp_code_mode_binding_level === "server" ? (
|
|
<div className="bg-muted border-border rounded-sm border p-4">
|
|
<div className="text-foreground space-y-1 font-mono text-xs">
|
|
<div>servers/</div>
|
|
<div className="pl-3">├─ calculator.py</div>
|
|
<div className="pl-3">├─ youtube.py</div>
|
|
<div className="pl-3">└─ weather.py</div>
|
|
</div>
|
|
<p className="text-muted-foreground mt-3 text-xs">
|
|
All tools per server in a single .py file
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="bg-muted border-border rounded-sm border p-4">
|
|
<div className="text-foreground space-y-1 font-mono text-xs">
|
|
<div>servers/</div>
|
|
<div className="pl-3">├─ calculator/</div>
|
|
<div className="pl-6">├─ add.py</div>
|
|
<div className="pl-6">└─ subtract.py</div>
|
|
<div className="pl-3">├─ youtube/</div>
|
|
<div className="pl-6">├─ GET_CHANNELS.py</div>
|
|
<div className="pl-6">└─ SEARCH_VIDEOS.py</div>
|
|
<div className="pl-3">└─ weather/</div>
|
|
<div className="pl-6">└─ get_forecast.py</div>
|
|
</div>
|
|
<p className="text-muted-foreground mt-3 text-xs">
|
|
Individual .py file for each tool
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end pt-2">
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={!hasChanges || isLoading || !hasSettingsUpdateAccess}
|
|
data-testid="mcp-settings-save-btn"
|
|
>
|
|
{isLoading ? "Saving..." : "Save Changes"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|