local-coding-agent 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- local_coding_agent/__init__.py +314 -0
- local_coding_agent/__main__.py +5 -0
- local_coding_agent/acp_server/__init__.py +38 -0
- local_coding_agent/acp_server/_codec.py +95 -0
- local_coding_agent/acp_server/_protocol.py +13 -0
- local_coding_agent/acp_server/_server.py +663 -0
- local_coding_agent/acp_server/_session.py +24 -0
- local_coding_agent/acp_server/_tools.py +126 -0
- local_coding_agent/ast_compactor.py +130 -0
- local_coding_agent/atomizer.py +77 -0
- local_coding_agent/benchmark/__init__.py +57 -0
- local_coding_agent/benchmark/_cases.py +307 -0
- local_coding_agent/benchmark/_judge.py +175 -0
- local_coding_agent/benchmark/_oracles.py +157 -0
- local_coding_agent/benchmark/_runner.py +259 -0
- local_coding_agent/benchmark/_stats.py +22 -0
- local_coding_agent/benchmark/_summary.py +143 -0
- local_coding_agent/benchmark_oracle_worker.py +169 -0
- local_coding_agent/calibration.py +154 -0
- local_coding_agent/capability.py +450 -0
- local_coding_agent/chat_compaction.py +276 -0
- local_coding_agent/cli/__init__.py +12 -0
- local_coding_agent/cli/_dispatch.py +377 -0
- local_coding_agent/cli/_handlers.py +320 -0
- local_coding_agent/cli/_handlers2.py +725 -0
- local_coding_agent/cli/_input.py +61 -0
- local_coding_agent/cli/_main.py +159 -0
- local_coding_agent/cli/_parser.py +327 -0
- local_coding_agent/context_manager.py +155 -0
- local_coding_agent/controller/__init__.py +18 -0
- local_coding_agent/controller/_constants.py +108 -0
- local_coding_agent/controller/_controller.py +1008 -0
- local_coding_agent/controller/_post_apply.py +216 -0
- local_coding_agent/delegator.py +180 -0
- local_coding_agent/desktop/__init__.py +8 -0
- local_coding_agent/desktop/app.py +100 -0
- local_coding_agent/desktop/assets/lucide.min.js +12 -0
- local_coding_agent/desktop/assets/tailwind.css +1 -0
- local_coding_agent/desktop/client_js.py +1271 -0
- local_coding_agent/desktop/components.py +555 -0
- local_coding_agent/desktop/server/__init__.py +37 -0
- local_coding_agent/desktop/server/_handlers.py +2178 -0
- local_coding_agent/desktop/server/_models.py +204 -0
- local_coding_agent/desktop/server/_server.py +347 -0
- local_coding_agent/desktop/server/_telemetry.py +87 -0
- local_coding_agent/desktop/styles.py +76 -0
- local_coding_agent/desktop/ui.py +53 -0
- local_coding_agent/doctor.py +413 -0
- local_coding_agent/hooks/__init__.py +19 -0
- local_coding_agent/hooks/_adapters.py +458 -0
- local_coding_agent/hooks/_bridge.py +346 -0
- local_coding_agent/hooks/_decision.py +26 -0
- local_coding_agent/lsp/__init__.py +60 -0
- local_coding_agent/lsp/_client.py +322 -0
- local_coding_agent/lsp/_fallback.py +297 -0
- local_coding_agent/lsp/_manager.py +173 -0
- local_coding_agent/lsp/_mock.py +200 -0
- local_coding_agent/lsp/_types.py +398 -0
- local_coding_agent/mcp_config.py +398 -0
- local_coding_agent/mcp_server.py +377 -0
- local_coding_agent/memory.py +140 -0
- local_coding_agent/mode_router.py +200 -0
- local_coding_agent/model_scanner.py +525 -0
- local_coding_agent/monitor.py +635 -0
- local_coding_agent/observation_policy.py +184 -0
- local_coding_agent/ollama_adapter.py +736 -0
- local_coding_agent/plan_mode.py +653 -0
- local_coding_agent/prescriptions.py +278 -0
- local_coding_agent/profiles.py +239 -0
- local_coding_agent/repository_tools.py +782 -0
- local_coding_agent/ripgrep.py +326 -0
- local_coding_agent/semantic_linter.py +122 -0
- local_coding_agent/service.py +405 -0
- local_coding_agent/session_events.py +684 -0
- local_coding_agent/session_query.py +578 -0
- local_coding_agent/skill_config.py +426 -0
- local_coding_agent/smoke.py +268 -0
- local_coding_agent/speculative_racing.py +85 -0
- local_coding_agent/spill.py +258 -0
- local_coding_agent/stats.py +255 -0
- local_coding_agent/stdio.py +132 -0
- local_coding_agent/subagent.py +516 -0
- local_coding_agent/task.py +90 -0
- local_coding_agent/task_store.py +142 -0
- local_coding_agent/tasks.py +240 -0
- local_coding_agent/terminal/__init__.py +35 -0
- local_coding_agent/terminal/_manager.py +156 -0
- local_coding_agent/terminal/_process.py +148 -0
- local_coding_agent/terminal/_session.py +452 -0
- local_coding_agent/terminal/_tools.py +233 -0
- local_coding_agent/terminal/_types.py +60 -0
- local_coding_agent/tool_call_parser.py +178 -0
- local_coding_agent/validators.py +563 -0
- local_coding_agent/vram_fit.py +240 -0
- local_coding_agent/worker_pool.py +516 -0
- local_coding_agent-1.1.0.dist-info/METADATA +202 -0
- local_coding_agent-1.1.0.dist-info/RECORD +101 -0
- local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
- local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
- local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
- local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""MCP client configuration generator and integrator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_CODEX_CLIENTS = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"chatgpt",
|
|
16
|
+
"chatgpt-desktop",
|
|
17
|
+
"codex",
|
|
18
|
+
"codex-desktop",
|
|
19
|
+
"codex-cli",
|
|
20
|
+
"openai",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_client_config_path(client: str, workspace: str | Path = ".") -> Path:
|
|
26
|
+
client_norm = client.lower().strip()
|
|
27
|
+
home = Path.home()
|
|
28
|
+
ws = Path(workspace).resolve()
|
|
29
|
+
|
|
30
|
+
if client_norm in ("claude", "claude-desktop", "claudedesktop", "claude-code"):
|
|
31
|
+
if sys.platform == "win32":
|
|
32
|
+
appdata = os.environ.get("APPDATA")
|
|
33
|
+
base = Path(appdata) if appdata else home / "AppData" / "Roaming"
|
|
34
|
+
return base / "Claude" / "claude_desktop_config.json"
|
|
35
|
+
if sys.platform == "darwin":
|
|
36
|
+
return home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
|
37
|
+
# Linux
|
|
38
|
+
config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
39
|
+
base = Path(config_home) if config_home else home / ".config"
|
|
40
|
+
return base / "Claude" / "claude_desktop_config.json"
|
|
41
|
+
|
|
42
|
+
if client_norm in ("cursor", "cursor-ide"):
|
|
43
|
+
return ws / ".cursor" / "mcp.json"
|
|
44
|
+
|
|
45
|
+
if client_norm in ("windsurf", "codeium"):
|
|
46
|
+
return home / ".codeium" / "windsurf" / "mcp_config.json"
|
|
47
|
+
|
|
48
|
+
if client_norm in ("cline", "cline-desktop", "roo", "roo-code", "vscode"):
|
|
49
|
+
# Check workspace .vscode/mcp.json or user-level .cline/mcp.json
|
|
50
|
+
if (ws / ".vscode").is_dir():
|
|
51
|
+
return ws / ".vscode" / "mcp.json"
|
|
52
|
+
return home / ".cline" / "mcp.json"
|
|
53
|
+
|
|
54
|
+
if client_norm in ("antigravity", "antigravity-desktop", "agy"):
|
|
55
|
+
return home / ".gemini" / "config" / "mcp_config.json"
|
|
56
|
+
|
|
57
|
+
if client_norm in ("opencode", "opencode-desktop", "opencode-cli"):
|
|
58
|
+
return home / ".config" / "opencode" / "opencode.jsonc"
|
|
59
|
+
|
|
60
|
+
if client_norm in _CODEX_CLIENTS:
|
|
61
|
+
return home / ".codex" / "config.toml"
|
|
62
|
+
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Unsupported MCP client: {client}. Supported: claude, cursor, windsurf, cline, antigravity, opencode, codex, chatgpt, vscode"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def detect_installed_clients(workspace: str | Path = ".") -> list[str]:
|
|
69
|
+
"""Detect available IDE / MCP clients in workspace and host environment."""
|
|
70
|
+
ws = Path(workspace).resolve()
|
|
71
|
+
home = Path.home()
|
|
72
|
+
detected: list[str] = []
|
|
73
|
+
|
|
74
|
+
# Check workspace directories
|
|
75
|
+
if (ws / ".cursor").is_dir():
|
|
76
|
+
detected.append("cursor")
|
|
77
|
+
if (ws / ".vscode").is_dir():
|
|
78
|
+
detected.append("cline")
|
|
79
|
+
|
|
80
|
+
# Check user-level configurations
|
|
81
|
+
try:
|
|
82
|
+
claude_path = get_client_config_path("claude", ws)
|
|
83
|
+
if claude_path.parent.is_dir() or claude_path.exists():
|
|
84
|
+
detected.append("claude")
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
windsurf_path = get_client_config_path("windsurf", ws)
|
|
90
|
+
if windsurf_path.parent.is_dir() or windsurf_path.exists():
|
|
91
|
+
detected.append("windsurf")
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
if (home / ".gemini").is_dir():
|
|
97
|
+
detected.append("antigravity")
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
if (home / ".config" / "opencode").is_dir() or (home / ".opencode").is_dir():
|
|
103
|
+
detected.append("opencode")
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
if (home / ".codex").is_dir():
|
|
109
|
+
detected.append("codex")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
return detected
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def generate_mcp_config_dict(
|
|
118
|
+
workspace: str | Path,
|
|
119
|
+
profile: str = "qwen3-8b-q6k",
|
|
120
|
+
endpoint: str | None = None,
|
|
121
|
+
command: str | None = None,
|
|
122
|
+
server_name: str = "local-coding-agent",
|
|
123
|
+
client: str = "generic",
|
|
124
|
+
) -> dict[str, Any]:
|
|
125
|
+
cmd = command or sys.executable
|
|
126
|
+
args = ["-m", "local_coding_agent", "serve-mcp", "--workspace", str(Path(workspace).resolve()), "--profile", profile]
|
|
127
|
+
if endpoint:
|
|
128
|
+
args.extend(["--endpoint", endpoint])
|
|
129
|
+
|
|
130
|
+
client_norm = client.lower().strip()
|
|
131
|
+
if client_norm in ("opencode", "opencode-desktop", "opencode-cli"):
|
|
132
|
+
return {
|
|
133
|
+
"mcp": {
|
|
134
|
+
server_name: {
|
|
135
|
+
"type": "local",
|
|
136
|
+
"command": [cmd, *args],
|
|
137
|
+
"enabled": True,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"mcpServers": {
|
|
144
|
+
server_name: {
|
|
145
|
+
"command": cmd,
|
|
146
|
+
"args": args,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _toml_key(value: str) -> str:
|
|
153
|
+
if re.fullmatch(r"[A-Za-z0-9_-]+", value):
|
|
154
|
+
return value
|
|
155
|
+
return json.dumps(value, ensure_ascii=False)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _toml_table_name(server_name: str) -> str:
|
|
159
|
+
return f"mcp_servers.{_toml_key(server_name)}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _render_codex_server_toml(server_name: str, server: dict[str, Any]) -> str:
|
|
163
|
+
args = ", ".join(json.dumps(str(arg), ensure_ascii=False) for arg in server["args"])
|
|
164
|
+
table = _toml_table_name(server_name)
|
|
165
|
+
return (
|
|
166
|
+
f"[{table}]\n"
|
|
167
|
+
f"command = {json.dumps(str(server['command']), ensure_ascii=False)}\n"
|
|
168
|
+
f"args = [{args}]\n"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _merge_codex_server_toml(existing: str, server_name: str, server: dict[str, Any]) -> str:
|
|
173
|
+
"""Replace one Codex MCP table while preserving unrelated TOML settings."""
|
|
174
|
+
replacement = _render_codex_server_toml(server_name, server).rstrip()
|
|
175
|
+
text = existing.rstrip()
|
|
176
|
+
table = _toml_table_name(server_name)
|
|
177
|
+
section_pattern = re.compile(r"(?m)^\[([^\]\r\n]+)\][ \t]*$")
|
|
178
|
+
sections = list(section_pattern.finditer(text))
|
|
179
|
+
target_index = next(
|
|
180
|
+
(
|
|
181
|
+
index
|
|
182
|
+
for index, section in enumerate(sections)
|
|
183
|
+
if section.group(1).strip() == table
|
|
184
|
+
),
|
|
185
|
+
None,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if target_index is None:
|
|
189
|
+
return f"{text}\n\n{replacement}\n" if text else f"{replacement}\n"
|
|
190
|
+
|
|
191
|
+
start = sections[target_index].start()
|
|
192
|
+
end = len(text)
|
|
193
|
+
for section in sections[target_index + 1 :]:
|
|
194
|
+
name = section.group(1).strip()
|
|
195
|
+
if not name.startswith(f"{table}."):
|
|
196
|
+
end = section.start()
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
prefix = text[:start].rstrip()
|
|
200
|
+
suffix = text[end:].lstrip()
|
|
201
|
+
parts = [part for part in (prefix, replacement, suffix) if part]
|
|
202
|
+
return "\n\n".join(parts) + "\n"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _read_json_config(path: Path) -> dict[str, Any]:
|
|
206
|
+
"""Read a JSON/JSONC config without treating corruption as empty data."""
|
|
207
|
+
raw = path.read_text(encoding="utf-8-sig")
|
|
208
|
+
cleaned_lines = []
|
|
209
|
+
for line in raw.splitlines():
|
|
210
|
+
stripped = line.strip()
|
|
211
|
+
if stripped.startswith("//") or stripped.startswith("#"):
|
|
212
|
+
continue
|
|
213
|
+
cleaned_lines.append(line)
|
|
214
|
+
loaded = json.loads("\n".join(cleaned_lines))
|
|
215
|
+
if not isinstance(loaded, dict):
|
|
216
|
+
raise ValueError("MCP config root must be a JSON object")
|
|
217
|
+
return loaded
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _atomic_write_text(path: Path, text: str) -> None:
|
|
221
|
+
"""Write a config through a same-directory temporary file and replace."""
|
|
222
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
223
|
+
try:
|
|
224
|
+
temporary.write_text(text, encoding="utf-8")
|
|
225
|
+
temporary.replace(path)
|
|
226
|
+
except Exception:
|
|
227
|
+
try:
|
|
228
|
+
temporary.unlink()
|
|
229
|
+
except OSError:
|
|
230
|
+
pass
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _config_read_failure(client: str, path: Path, error: Exception, dry_run: bool) -> dict[str, Any]:
|
|
235
|
+
return {
|
|
236
|
+
"client": client,
|
|
237
|
+
"path": str(path),
|
|
238
|
+
"dry_run": dry_run,
|
|
239
|
+
"written": False,
|
|
240
|
+
"status": "failed",
|
|
241
|
+
"error": f"Cannot read existing MCP config without risking data loss: {error}",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def integrate_mcp_config(
|
|
246
|
+
client: str,
|
|
247
|
+
workspace: str | Path = ".",
|
|
248
|
+
profile: str = "qwen3-8b-q6k",
|
|
249
|
+
target_path: Path | None = None,
|
|
250
|
+
dry_run: bool = False,
|
|
251
|
+
endpoint: str | None = None,
|
|
252
|
+
server_name: str = "local-coding-agent",
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
client_norm = client.lower().strip()
|
|
255
|
+
|
|
256
|
+
if client_norm in ("auto", "all", "*"):
|
|
257
|
+
targets = detect_installed_clients(workspace)
|
|
258
|
+
sub_results = []
|
|
259
|
+
all_written = True
|
|
260
|
+
for tgt in targets:
|
|
261
|
+
try:
|
|
262
|
+
sub_res = integrate_mcp_config(
|
|
263
|
+
client=tgt,
|
|
264
|
+
workspace=workspace,
|
|
265
|
+
profile=profile,
|
|
266
|
+
dry_run=dry_run,
|
|
267
|
+
endpoint=endpoint,
|
|
268
|
+
server_name=server_name,
|
|
269
|
+
)
|
|
270
|
+
except Exception as error:
|
|
271
|
+
try:
|
|
272
|
+
failed_path = str(get_client_config_path(tgt, workspace))
|
|
273
|
+
except Exception:
|
|
274
|
+
failed_path = ""
|
|
275
|
+
sub_res = {
|
|
276
|
+
"client": tgt,
|
|
277
|
+
"path": failed_path,
|
|
278
|
+
"dry_run": dry_run,
|
|
279
|
+
"written": False,
|
|
280
|
+
"status": "failed",
|
|
281
|
+
"error": str(error),
|
|
282
|
+
}
|
|
283
|
+
sub_results.append(sub_res)
|
|
284
|
+
if not sub_res.get("written", False):
|
|
285
|
+
all_written = False
|
|
286
|
+
return {
|
|
287
|
+
"client": client,
|
|
288
|
+
"detected_clients": targets,
|
|
289
|
+
"results": sub_results,
|
|
290
|
+
"dry_run": dry_run,
|
|
291
|
+
"written": all_written if not dry_run else False,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
resolved_path = target_path if target_path is not None else get_client_config_path(client, workspace)
|
|
295
|
+
is_opencode = client_norm in ("opencode", "opencode-desktop", "opencode-cli") or resolved_path.name.startswith("opencode.")
|
|
296
|
+
is_toml = resolved_path.suffix.lower() == ".toml"
|
|
297
|
+
|
|
298
|
+
snippet = generate_mcp_config_dict(
|
|
299
|
+
workspace=workspace,
|
|
300
|
+
profile=profile,
|
|
301
|
+
endpoint=endpoint,
|
|
302
|
+
server_name=server_name,
|
|
303
|
+
client="opencode" if is_opencode else client_norm,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if is_toml:
|
|
307
|
+
server = snippet["mcpServers"][server_name]
|
|
308
|
+
if resolved_path.exists():
|
|
309
|
+
try:
|
|
310
|
+
existing = resolved_path.read_text(encoding="utf-8-sig")
|
|
311
|
+
except (OSError, UnicodeError) as error:
|
|
312
|
+
return _config_read_failure(client, resolved_path, error, dry_run)
|
|
313
|
+
else:
|
|
314
|
+
existing = ""
|
|
315
|
+
merged_text = _merge_codex_server_toml(existing, server_name, server)
|
|
316
|
+
if dry_run:
|
|
317
|
+
return {
|
|
318
|
+
"client": client,
|
|
319
|
+
"path": str(resolved_path),
|
|
320
|
+
"dry_run": True,
|
|
321
|
+
"written": False,
|
|
322
|
+
"config": merged_text,
|
|
323
|
+
"snippet": snippet,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
resolved_path.parent.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
try:
|
|
328
|
+
_atomic_write_text(resolved_path, merged_text)
|
|
329
|
+
except OSError as error:
|
|
330
|
+
return {
|
|
331
|
+
"client": client,
|
|
332
|
+
"path": str(resolved_path),
|
|
333
|
+
"dry_run": False,
|
|
334
|
+
"written": False,
|
|
335
|
+
"status": "failed",
|
|
336
|
+
"error": f"Cannot write MCP config atomically: {error}",
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
"client": client,
|
|
340
|
+
"path": str(resolved_path),
|
|
341
|
+
"dry_run": False,
|
|
342
|
+
"written": True,
|
|
343
|
+
"config": merged_text,
|
|
344
|
+
"snippet": snippet,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
merged_data: dict[str, Any] = {}
|
|
348
|
+
|
|
349
|
+
if resolved_path.exists():
|
|
350
|
+
try:
|
|
351
|
+
merged_data = _read_json_config(resolved_path)
|
|
352
|
+
except (OSError, UnicodeError, ValueError, json.JSONDecodeError) as error:
|
|
353
|
+
return _config_read_failure(client, resolved_path, error, dry_run)
|
|
354
|
+
|
|
355
|
+
if is_opencode:
|
|
356
|
+
if "mcp" not in merged_data or not isinstance(merged_data["mcp"], dict):
|
|
357
|
+
merged_data["mcp"] = {}
|
|
358
|
+
# Clean up any errant mcpServers key in opencode config which violates opencode.json schema
|
|
359
|
+
if "mcpServers" in merged_data:
|
|
360
|
+
del merged_data["mcpServers"]
|
|
361
|
+
merged_data["mcp"][server_name] = snippet["mcp"][server_name]
|
|
362
|
+
else:
|
|
363
|
+
if "mcpServers" not in merged_data or not isinstance(merged_data["mcpServers"], dict):
|
|
364
|
+
merged_data["mcpServers"] = {}
|
|
365
|
+
merged_data["mcpServers"][server_name] = snippet["mcpServers"][server_name]
|
|
366
|
+
|
|
367
|
+
if dry_run:
|
|
368
|
+
return {
|
|
369
|
+
"client": client,
|
|
370
|
+
"path": str(resolved_path),
|
|
371
|
+
"dry_run": True,
|
|
372
|
+
"written": False,
|
|
373
|
+
"config": merged_data,
|
|
374
|
+
"snippet": snippet,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Ensure parent directory exists
|
|
378
|
+
resolved_path.parent.mkdir(parents=True, exist_ok=True)
|
|
379
|
+
try:
|
|
380
|
+
_atomic_write_text(resolved_path, json.dumps(merged_data, indent=2, ensure_ascii=False))
|
|
381
|
+
except OSError as error:
|
|
382
|
+
return {
|
|
383
|
+
"client": client,
|
|
384
|
+
"path": str(resolved_path),
|
|
385
|
+
"dry_run": False,
|
|
386
|
+
"written": False,
|
|
387
|
+
"status": "failed",
|
|
388
|
+
"error": f"Cannot write MCP config atomically: {error}",
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
"client": client,
|
|
393
|
+
"path": str(resolved_path),
|
|
394
|
+
"dry_run": False,
|
|
395
|
+
"written": True,
|
|
396
|
+
"config": merged_data,
|
|
397
|
+
"snippet": snippet,
|
|
398
|
+
}
|