Skip to content
Merged
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
14 changes: 12 additions & 2 deletions backend/app/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand All @@ -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"}

Expand Down
4 changes: 3 additions & 1 deletion backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
82 changes: 74 additions & 8 deletions frontend/src/features/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -70,6 +78,7 @@ export function SettingsPage({
[chatProfile, setChatProfile] = useState(""),
[codingAgent, setCodingAgent] = useState<ApplicationSettings["coding_agent_provider"]>("none"),
[mcpConfig, setMcpConfig] = useState(""),
[mcpOpen, setMcpOpen] = useState(false),
[dataPath, setDataPath] = useState(""),
[dataPathDraft, setDataPathDraft] = useState(""),
[dataSize, setDataSize] = useState<number | null>(null),
Expand Down Expand Up @@ -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<string, unknown>
: {};
return {
id: name,
name,
url: typeof details.url === "string" ? details.url : "—",
enabled: details.enabled !== false && details.disabled !== true,
};
});
} catch { return []; }
}, [mcpConfig]);
const mcpColumns = useMemo<Column<McpServer>[]>(() => [
{ id: "name", header: settingsCopy.mcp.name, render: (server) => <strong>{server.name}</strong>, 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<ApplicationData>("/api/application-data", "PUT", { path: dataPathDraft })
Expand Down Expand Up @@ -249,11 +285,22 @@ export function SettingsPage({
<option value="midnight">Midnight</option>
</select>
</label>
<div className="setting-row setting-row--stacked">
<span><strong>{settingsCopy.codingAgent.mcpConfig}</strong><small>{settingsCopy.codingAgent.mcpConfigDescription}</small></span>
<JsonEditor value={mcpConfig} onChange={setMcpConfig} label={settingsCopy.codingAgent.mcpConfig} />
<div className="setting-actions"><button className="ghost" onClick={() => api("/api/assistant-mcp-config/open-vscode", "POST").catch((error) => pushToast(error.message))}><ExternalLink size={14} />{settingsCopy.codingAgent.openInVsCode}</button><button className="approve" onClick={saveMcpConfig}><Save size={14} />{settingsCopy.codingAgent.saveMcpConfig}</button></div>
</section>
<section className="panel app-settings">
<div className="panel-title-action">
<div className="panel-title-action__copy">
<PanelHeader title={<SectionInfo title={settingsCopy.mcp.title} description={settingsCopy.mcp.description} />} />
<p className="hint section-description">{settingsCopy.mcp.description}</p>
</div>
<button className="approve" onClick={() => setMcpOpen(true)}><Pencil size={14} />{settingsCopy.mcp.edit}</button>
</div>
<DataTable
className="mcp-server-table"
columns={mcpColumns}
rows={mcpServers}
empty={settingsCopy.mcp.noServers}
gridTemplateColumns="minmax(140px,.8fr) minmax(280px,2fr) 110px"
/>
</section>
<section className="panel app-settings app-data-settings">
<PanelHeader title={<SectionInfo title={settingsCopy.storage.title} description={sectionDetails.applicationData} />} />
Expand Down Expand Up @@ -370,6 +417,25 @@ export function SettingsPage({
<p className="setting-prompt-preview">{prompt || l.empty}</p>
</section>
<OrbitLogs logs={logs} locale={locale} />
<Modal open={mcpOpen} title={settingsCopy.mcp.title} onClose={() => setMcpOpen(false)}>
<div className="modal-form">
<JsonEditor value={mcpConfig} onChange={setMcpConfig} label={settingsCopy.mcp.title} />
<div className="modal-actions">
<button className="ghost" onClick={() => setMcpConfig(defaultMcpConfig)}>
<RotateCcw size={14} />
{settingsCopy.mcp.reset}
</button>
<button className="ghost" onClick={() => api("/api/assistant-mcp-config/open-vscode", "POST").catch((error) => pushToast(error.message))}>
<ExternalLink size={14} />
{settingsCopy.mcp.openInVsCode}
</button>
<button className="approve" onClick={saveMcpConfig}>
<Save size={14} />
{settingsCopy.mcp.save}
</button>
</div>
</div>
</Modal>
<Modal open={open} title={l.title} onClose={() => setOpen(false)}>
<div className="modal-form">
<label className="modal-setting-row">
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,22 @@
"select": "Coding Agent",
"none": "Not selected",
"save": "Save",
"saved": "Coding Agent saved.",
"mcpConfig": "MCP configuration",
"mcpConfigDescription": "Edit the JSON configuration file used for local coding-agent MCP connections.",
"saved": "Coding Agent saved."
},
"mcp": {
"title": "MCP connections",
"description": "Manage MCP servers available to local coding agents.",
"edit": "Edit",
"save": "Save",
"reset": "Reset",
"openInVsCode": "Open in VS Code",
"saveMcpConfig": "Save MCP configuration",
"mcpConfigSaved": "MCP configuration saved."
"name": "Name",
"url": "URL",
"status": "Status",
"enabled": "Enabled",
"disabled": "Disabled",
"noServers": "No servers configured",
"saved": "MCP configuration saved."
},
"storage": {
"title": "Application data",
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/locales/languages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,22 @@
"select": "Coding Agent",
"none": "未選択",
"save": "保存",
"saved": "Coding Agent を保存しました。",
"mcpConfig": "MCP 設定",
"mcpConfigDescription": "ローカルのコーディングエージェント MCP 接続に使う JSON 設定ファイルを編集します。",
"saved": "Coding Agent を保存しました。"
},
"mcp": {
"title": "MCP 接続",
"description": "ローカルのコーディングエージェントが使用する MCP サーバーを管理します。",
"edit": "編集",
"save": "保存",
"reset": "リセット",
"openInVsCode": "VS Code で開く",
"saveMcpConfig": "MCP 設定を保存",
"mcpConfigSaved": "MCP 設定を保存しました。"
"name": "名前",
"url": "URL",
"status": "状態",
"enabled": "有効",
"disabled": "無効",
"noServers": "構成済みのサーバーはありません",
"saved": "MCP 設定を保存しました。"
},
"storage": {
"title": "アプリデータ",
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/locales/languages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,22 @@
"select": "코딩 에이전트",
"none": "미선택",
"save": "저장",
"saved": "코딩 에이전트를 저장했습니다.",
"mcpConfig": "MCP 설정",
"mcpConfigDescription": "로컬 코딩 에이전트 MCP 연결에 사용하는 JSON 설정 파일을 편집합니다.",
"saved": "코딩 에이전트를 저장했습니다."
},
"mcp": {
"title": "MCP 연결",
"description": "로컬 코딩 에이전트가 사용할 MCP 서버를 관리합니다.",
"edit": "편집",
"save": "저장",
"reset": "초기화",
"openInVsCode": "VS Code에서 열기",
"saveMcpConfig": "MCP 설정 저장",
"mcpConfigSaved": "MCP 설정을 저장했습니다."
"name": "이름",
"url": "URL",
"status": "상태",
"enabled": "활성화됨",
"disabled": "비활성화됨",
"noServers": "구성된 서버 없음",
"saved": "MCP 설정을 저장했습니다."
},
"storage": {
"title": "앱 데이터",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/theme-overrides.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading