From 87389f3bed6a4afb0d62b69b1b184895b0c3e2a8 Mon Sep 17 00:00:00 2001 From: forthfate Date: Sat, 19 Sep 2026 12:09:34 +0900 Subject: [PATCH] feat: separate MCP connection settings --- backend/app/store.py | 14 ++++- backend/tests/test_api.py | 4 +- frontend/src/features/settings/page.tsx | 82 ++++++++++++++++++++++--- frontend/src/locales/languages/en.json | 20 ++++-- frontend/src/locales/languages/ja.json | 20 ++++-- frontend/src/locales/languages/ko.json | 20 ++++-- frontend/src/theme-overrides.css | 1 + 7 files changed, 135 insertions(+), 26 deletions(-) diff --git a/backend/app/store.py b/backend/app/store.py index 670e37d..f145138 100644 --- a/backend/app/store.py +++ b/backend/app/store.py @@ -29,6 +29,7 @@ from orbit import load_bundle +from .assistant_tools import DEFAULT_MCP_URL from .assistant_tools import normalize_settings as normalize_assistant_tools from .models import PHASE_ALIASES, Run, Step, Workflow from .observability import configure_telemetry @@ -1131,7 +1132,14 @@ def open_runner_in_vscode(self, runner_id: str) -> dict[str, str]: def assistant_mcp_config(self) -> dict[str, str]: if not ASSISTANT_MCP_CONFIG.exists(): - return {"path": str(ASSISTANT_MCP_CONFIG), "content": '{\n "mcpServers": {}\n}\n'} + return { + "path": str(ASSISTANT_MCP_CONFIG), + "content": json.dumps( + {"mcpServers": {"openorbit": {"url": DEFAULT_MCP_URL}}}, + indent=2, + ) + + "\n", + } return { "path": str(ASSISTANT_MCP_CONFIG), "content": ASSISTANT_MCP_CONFIG.read_text(encoding="utf-8"), @@ -1152,7 +1160,9 @@ def save_assistant_mcp_config(self, content: str) -> dict[str, str]: def open_assistant_mcp_config_in_vscode(self) -> dict[str, str]: if not ASSISTANT_MCP_CONFIG.exists(): - self.save_assistant_mcp_config('{"mcpServers": {}}') + self.save_assistant_mcp_config( + json.dumps({"mcpServers": {"openorbit": {"url": DEFAULT_MCP_URL}}}) + ) self._open_in_vscode(ASSISTANT_MCP_CONFIG) return {"status": "opened"} diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index fc141c6..1b6b1bf 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -2194,7 +2194,9 @@ def test_assistant_mcp_configuration_uses_a_valid_default_and_persists_json(tmp_ monkeypatch.setattr(store_module, "ASSISTANT_MCP_CONFIG", mcp_config) store = store_module.ConsoleStore() - assert store.assistant_mcp_config()["content"] == '{\n "mcpServers": {}\n}\n' + assert store.assistant_mcp_config()["content"] == ( + '{\n "mcpServers": {\n "openorbit": {\n "url": "http://127.0.0.1:3000/mcp/"\n }\n }\n}\n' + ) saved = store.save_assistant_mcp_config('{"mcpServers": {"orbit": {"command": "uv"}}}') assert saved["content"] == '{\n "mcpServers": {\n "orbit": {\n "command": "uv"\n }\n }\n}\n' diff --git a/frontend/src/features/settings/page.tsx b/frontend/src/features/settings/page.tsx index 71dddbf..41de328 100644 --- a/frontend/src/features/settings/page.tsx +++ b/frontend/src/features/settings/page.tsx @@ -1,10 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { AlertTriangle, Database, ExternalLink, Pencil, Save } from "lucide-react"; +import { AlertTriangle, Database, ExternalLink, Pencil, RotateCcw, Save } from "lucide-react"; import type { Locale } from "../../locales"; import { localeMessages, localeOptions, locales } from "../../locales"; import { Modal } from "../../components/ui/modal"; import { PanelHeader } from "../../components/ui/page-header"; import { SectionInfo } from "../../components/ui/section-info"; +import { DataTable, type Column } from "../../components/ui/data-table"; import type { OrbitLog, Settings } from "../../domain/models"; import { api } from "../../services/api"; import { useToast } from "../../components/ui/toast-context"; @@ -24,9 +25,16 @@ type ApplicationData = { path: string; size_bytes: number }; type ManagerCopy = { title:string; description:string; warning:string; edit:string; content:string; save:string; cancel:string; empty:string; saved:string }; type ProfileCopy = { title:string; description:string; create:string; edit:string; empty:string; delete:string; chatProfile:string; chatProfileHint:string; selectChatProfile:string; saveChatProfile:string; chatProfileSaved:string }; -type CodingAgentCopy = { title:string; description:string; select:string; none:string; save:string; saved:string; mcpConfig:string; mcpConfigDescription:string; openInVsCode:string; saveMcpConfig:string; mcpConfigSaved:string }; +type CodingAgentCopy = { title:string; description:string; select:string; none:string; save:string; saved:string }; +type McpCopy = { title:string; description:string; edit:string; save:string; reset:string; openInVsCode:string; name:string; url:string; status:string; enabled:string; disabled:string; noServers:string; saved:string }; type StorageCopy = { title:string; description:string; location:string; locationHint:string; size:string; calculating:string; save:string; saved:string }; -type SettingsCopy = { manager: ManagerCopy; profiles: ProfileCopy; codingAgent: CodingAgentCopy; storage: StorageCopy }; +type SettingsCopy = { manager: ManagerCopy; profiles: ProfileCopy; codingAgent: CodingAgentCopy; mcp: McpCopy; storage: StorageCopy }; +type McpServer = { id: string; name: string; url: string; enabled: boolean }; +const defaultMcpConfig = JSON.stringify( + { mcpServers: { openorbit: { url: "http://127.0.0.1:3000/mcp/" } } }, + null, + 2, +); const bytes = (value: number) => { const units = ["B", "KB", "MB", "GB", "TB"]; const index = value ? Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1) : 0; @@ -70,6 +78,7 @@ export function SettingsPage({ [chatProfile, setChatProfile] = useState(""), [codingAgent, setCodingAgent] = useState("none"), [mcpConfig, setMcpConfig] = useState(""), + [mcpOpen, setMcpOpen] = useState(false), [dataPath, setDataPath] = useState(""), [dataPathDraft, setDataPathDraft] = useState(""), [dataSize, setDataSize] = useState(null), @@ -132,7 +141,34 @@ export function SettingsPage({ pushToast(settingsCopy.codingAgent.saved, "success"); }) .catch((error) => pushToast(error.message)); - const saveMcpConfig = () => api<{ content: string }>("/api/assistant-mcp-config", "PUT", { content: mcpConfig }).then((value) => { setMcpConfig(value.content); pushToast(settingsCopy.codingAgent.mcpConfigSaved, "success"); }).catch((error) => pushToast(error.message)); + const mcpServers = useMemo(() => { + try { + const value = JSON.parse(mcpConfig) as { mcpServers?: unknown }; + if (!value.mcpServers || typeof value.mcpServers !== "object" || Array.isArray(value.mcpServers)) return []; + return Object.entries(value.mcpServers).map(([name, config]) => { + const details = config && typeof config === "object" && !Array.isArray(config) + ? config as Record + : {}; + return { + id: name, + name, + url: typeof details.url === "string" ? details.url : "—", + enabled: details.enabled !== false && details.disabled !== true, + }; + }); + } catch { return []; } + }, [mcpConfig]); + const mcpColumns = useMemo[]>(() => [ + { id: "name", header: settingsCopy.mcp.name, render: (server) => {server.name}, sortValue: (server) => server.name }, + { id: "url", header: settingsCopy.mcp.url, render: (server) => server.url, sortValue: (server) => server.url }, + { + id: "status", + header: settingsCopy.mcp.status, + render: (server) => server.enabled ? settingsCopy.mcp.enabled : settingsCopy.mcp.disabled, + sortValue: (server) => server.enabled, + }, + ], [settingsCopy.mcp]); + const saveMcpConfig = () => api<{ content: string }>("/api/assistant-mcp-config", "PUT", { content: mcpConfig }).then((value) => { setMcpConfig(value.content); setMcpOpen(false); pushToast(settingsCopy.mcp.saved, "success"); }).catch((error) => pushToast(error.message)); const saveDataLocation = () => { setDataLoading(true); api("/api/application-data", "PUT", { path: dataPathDraft }) @@ -249,11 +285,22 @@ export function SettingsPage({ -
- {settingsCopy.codingAgent.mcpConfig}{settingsCopy.codingAgent.mcpConfigDescription} - -
+ +
+
+
+ } /> +

{settingsCopy.mcp.description}

+
+
+
} /> @@ -370,6 +417,25 @@ export function SettingsPage({

{prompt || l.empty}

+ setMcpOpen(false)}> +
+ +
+ + + +
+
+
setOpen(false)}>