agencode 0.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.
@@ -0,0 +1,513 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timezone
6
+ import inspect
7
+ from pathlib import Path
8
+ import sqlite3
9
+ from threading import RLock
10
+ from typing import Any
11
+ from uuid import uuid4
12
+
13
+ from langgraph.checkpoint.memory import InMemorySaver, PersistentDict
14
+
15
+ try:
16
+ from langgraph.checkpoint.sqlite import SqliteSaver
17
+ except ImportError: # pragma: no cover - exercised via fallback tests in this env.
18
+ SqliteSaver = None
19
+
20
+ try:
21
+ import aiosqlite
22
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
23
+ except ImportError: # pragma: no cover - exercised via fallback tests in this env.
24
+ aiosqlite = None
25
+ AsyncSqliteSaver = None
26
+
27
+
28
+ @dataclass(slots=True)
29
+ class ChatTurn:
30
+ role: str
31
+ content: str
32
+ created_at: str
33
+ agent_name: str | None = None
34
+ thread_id: str | None = None
35
+
36
+
37
+ @dataclass(slots=True)
38
+ class ChatThreadSummary:
39
+ agent_name: str | None
40
+ thread_id: str | None
41
+ turn_count: int
42
+ last_role: str
43
+ last_content: str
44
+ last_created_at: str
45
+
46
+
47
+ _CHECKPOINTERS: dict[Path, Any] = {}
48
+ _ASYNC_CHECKPOINTERS: dict[Path, Any] = {}
49
+
50
+
51
+ class PersistentInMemoryCheckpointer(InMemorySaver):
52
+ """File-backed LangGraph checkpointer using LangGraph's persistent dict helper."""
53
+
54
+ def __init__(self, base_path: Path) -> None:
55
+ super().__init__()
56
+ self.base_path = base_path
57
+ self.base_path.parent.mkdir(parents=True, exist_ok=True)
58
+ self._sync_lock = RLock()
59
+ self.storage = self._load_dict(
60
+ self.base_path.with_suffix(".storage.pkl"),
61
+ lambda: defaultdict(dict),
62
+ )
63
+ self.writes = self._load_dict(
64
+ self.base_path.with_suffix(".writes.pkl"),
65
+ dict,
66
+ )
67
+ self.blobs = self._load_dict(
68
+ self.base_path.with_suffix(".blobs.pkl"),
69
+ dict,
70
+ )
71
+
72
+ def put(
73
+ self,
74
+ config: dict[str, Any],
75
+ checkpoint: Any,
76
+ metadata: Any,
77
+ new_versions: Any,
78
+ ) -> dict[str, Any]:
79
+ with self._sync_lock:
80
+ updated = super().put(config, checkpoint, metadata, new_versions)
81
+ self._sync()
82
+ return updated
83
+
84
+ def put_writes(
85
+ self,
86
+ config: dict[str, Any],
87
+ writes: Any,
88
+ task_id: str,
89
+ task_path: str = "",
90
+ ) -> None:
91
+ with self._sync_lock:
92
+ super().put_writes(config, writes, task_id, task_path=task_path)
93
+ self._sync()
94
+
95
+ def delete_thread(self, thread_id: str) -> None:
96
+ with self._sync_lock:
97
+ super().delete_thread(thread_id)
98
+ self._sync()
99
+
100
+ def _load_dict(self, path: Path, default_factory: Any) -> PersistentDict:
101
+ store = PersistentDict(default_factory, filename=str(path))
102
+ if path.exists():
103
+ store.load()
104
+ return store
105
+
106
+ def _sync(self) -> None:
107
+ self.storage.sync()
108
+ self.writes.sync()
109
+ self.blobs.sync()
110
+
111
+
112
+ def ensure_session_db(sessions_dir: str, session_name: str = "default") -> Path:
113
+ sessions_path = Path(sessions_dir).expanduser()
114
+ sessions_path.mkdir(parents=True, exist_ok=True)
115
+ path = sessions_path / f"{session_name}.sqlite3"
116
+ connection = sqlite3.connect(path)
117
+ try:
118
+ connection.execute("CREATE TABLE IF NOT EXISTS session_meta (key TEXT PRIMARY KEY, value TEXT)")
119
+ connection.execute(
120
+ """
121
+ CREATE TABLE IF NOT EXISTS chat_history (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ role TEXT NOT NULL,
124
+ content TEXT NOT NULL,
125
+ created_at TEXT NOT NULL
126
+ )
127
+ """
128
+ )
129
+ columns = {
130
+ row[1]
131
+ for row in connection.execute("PRAGMA table_info(chat_history)").fetchall()
132
+ }
133
+ if "agent_name" not in columns:
134
+ connection.execute("ALTER TABLE chat_history ADD COLUMN agent_name TEXT")
135
+ if "thread_id" not in columns:
136
+ connection.execute("ALTER TABLE chat_history ADD COLUMN thread_id TEXT")
137
+ connection.commit()
138
+ finally:
139
+ connection.close()
140
+ return path
141
+
142
+
143
+ def append_chat_turn(
144
+ sessions_dir: str,
145
+ role: str,
146
+ content: str,
147
+ session_name: str = "default",
148
+ *,
149
+ agent_name: str | None = None,
150
+ thread_id: str | None = None,
151
+ ) -> None:
152
+ path = ensure_session_db(sessions_dir, session_name=session_name)
153
+ created_at = datetime.now(timezone.utc).isoformat()
154
+ connection = sqlite3.connect(path)
155
+ try:
156
+ connection.execute(
157
+ """
158
+ INSERT INTO chat_history(role, content, created_at, agent_name, thread_id)
159
+ VALUES (?, ?, ?, ?, ?)
160
+ """,
161
+ (role, content, created_at, agent_name, thread_id),
162
+ )
163
+ connection.commit()
164
+ finally:
165
+ connection.close()
166
+
167
+
168
+ def delete_chat_thread(
169
+ sessions_dir: str,
170
+ session_name: str = "default",
171
+ *,
172
+ agent_name: str | None = None,
173
+ thread_id: str | None = None,
174
+ ) -> int:
175
+ if thread_id is None:
176
+ return 0
177
+ path = ensure_session_db(sessions_dir, session_name=session_name)
178
+ connection = sqlite3.connect(path)
179
+ try:
180
+ where_clauses = ["thread_id = ?"]
181
+ params: list[Any] = [thread_id]
182
+ if agent_name is not None:
183
+ where_clauses.append("agent_name = ?")
184
+ params.append(agent_name)
185
+ cursor = connection.execute(
186
+ f"DELETE FROM chat_history WHERE {' AND '.join(where_clauses)}",
187
+ params,
188
+ )
189
+ connection.commit()
190
+ return int(cursor.rowcount or 0)
191
+ finally:
192
+ connection.close()
193
+
194
+
195
+ def load_chat_history(
196
+ sessions_dir: str,
197
+ session_name: str = "default",
198
+ limit: int = 50,
199
+ *,
200
+ agent_name: str | None = None,
201
+ thread_id: str | None = None,
202
+ ) -> list[ChatTurn]:
203
+ path = ensure_session_db(sessions_dir, session_name=session_name)
204
+ connection = sqlite3.connect(path)
205
+ try:
206
+ where_clauses: list[str] = []
207
+ params: list[Any] = []
208
+ if agent_name is not None:
209
+ where_clauses.append("agent_name = ?")
210
+ params.append(agent_name)
211
+ if thread_id is not None:
212
+ where_clauses.append("thread_id = ?")
213
+ params.append(thread_id)
214
+ where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
215
+ rows = connection.execute(
216
+ f"""
217
+ SELECT role, content, created_at, agent_name, thread_id
218
+ FROM chat_history
219
+ {where_sql}
220
+ ORDER BY id DESC
221
+ LIMIT ?
222
+ """,
223
+ [*params, limit],
224
+ ).fetchall()
225
+ finally:
226
+ connection.close()
227
+ return [
228
+ ChatTurn(
229
+ role=row[0],
230
+ content=row[1],
231
+ created_at=row[2],
232
+ agent_name=row[3],
233
+ thread_id=row[4],
234
+ )
235
+ for row in reversed(rows)
236
+ ]
237
+
238
+
239
+ def list_chat_threads(
240
+ sessions_dir: str,
241
+ session_name: str = "default",
242
+ limit: int = 50,
243
+ *,
244
+ agent_name: str | None = None,
245
+ thread_id: str | None = None,
246
+ query: str | None = None,
247
+ ) -> list[ChatThreadSummary]:
248
+ path = ensure_session_db(sessions_dir, session_name=session_name)
249
+ connection = sqlite3.connect(path)
250
+ try:
251
+ grouped_where_clauses: list[str] = []
252
+ grouped_params: list[Any] = []
253
+ if agent_name is not None:
254
+ grouped_where_clauses.append("agent_name = ?")
255
+ grouped_params.append(agent_name)
256
+ if thread_id is not None:
257
+ grouped_where_clauses.append("thread_id = ?")
258
+ grouped_params.append(thread_id)
259
+ grouped_where_sql = f"WHERE {' AND '.join(grouped_where_clauses)}" if grouped_where_clauses else ""
260
+
261
+ latest_where_clauses: list[str] = []
262
+ latest_params: list[Any] = []
263
+ if query:
264
+ pattern = f"%{query}%"
265
+ latest_where_clauses.append(
266
+ "(COALESCE(latest.agent_name, '') LIKE ? OR COALESCE(latest.thread_id, '') LIKE ? OR latest.content LIKE ?)"
267
+ )
268
+ latest_params.extend([pattern, pattern, pattern])
269
+ latest_where_sql = f"WHERE {' AND '.join(latest_where_clauses)}" if latest_where_clauses else ""
270
+ rows = connection.execute(
271
+ f"""
272
+ SELECT latest.agent_name, latest.thread_id, grouped.turn_count, latest.role, latest.content, latest.created_at
273
+ FROM (
274
+ SELECT agent_name, thread_id, COUNT(*) AS turn_count, MAX(id) AS last_id
275
+ FROM chat_history
276
+ {grouped_where_sql}
277
+ GROUP BY agent_name, thread_id
278
+ ) AS grouped
279
+ JOIN chat_history AS latest ON latest.id = grouped.last_id
280
+ {latest_where_sql}
281
+ ORDER BY latest.id DESC
282
+ LIMIT ?
283
+ """,
284
+ [*grouped_params, *latest_params, limit],
285
+ ).fetchall()
286
+ finally:
287
+ connection.close()
288
+ return [
289
+ ChatThreadSummary(
290
+ agent_name=row[0],
291
+ thread_id=row[1],
292
+ turn_count=row[2],
293
+ last_role=row[3],
294
+ last_content=row[4],
295
+ last_created_at=row[5],
296
+ )
297
+ for row in rows
298
+ ]
299
+
300
+
301
+ def get_thread_id(agent_name: str, session_name: str = "default") -> str:
302
+ return f"{session_name}:{agent_name}"
303
+
304
+
305
+ def create_thread_id(agent_name: str, session_name: str = "default") -> str:
306
+ return f"{get_thread_id(agent_name, session_name=session_name)}:{uuid4().hex[:8]}"
307
+
308
+
309
+ def resolve_thread_id(
310
+ agent_name: str,
311
+ session_name: str = "default",
312
+ *,
313
+ thread_id: str | None = None,
314
+ new_thread: bool = False,
315
+ ) -> str:
316
+ if thread_id is not None and new_thread:
317
+ raise ValueError("thread_id and new_thread are mutually exclusive")
318
+ if thread_id is not None:
319
+ return thread_id
320
+ if new_thread:
321
+ return create_thread_id(agent_name, session_name=session_name)
322
+ return get_thread_id(agent_name, session_name=session_name)
323
+
324
+
325
+ def build_thread_config(
326
+ agent_name: str,
327
+ session_name: str = "default",
328
+ *,
329
+ thread_id: str | None = None,
330
+ ) -> dict[str, dict[str, str]]:
331
+ return {"configurable": {"thread_id": resolve_thread_id(agent_name, session_name=session_name, thread_id=thread_id)}}
332
+
333
+
334
+ def describe_langgraph_checkpointer(
335
+ sessions_dir: str,
336
+ session_name: str = "default",
337
+ ) -> str:
338
+ if _should_use_sqlite_checkpointer(sessions_dir, session_name=session_name):
339
+ return "sqlite"
340
+ return "persistent-memory"
341
+
342
+
343
+ def clear_langgraph_checkpointer_cache() -> None:
344
+ for checkpointer in _CHECKPOINTERS.values():
345
+ _close_checkpointer_connection(checkpointer)
346
+ for checkpointer in _ASYNC_CHECKPOINTERS.values():
347
+ _close_checkpointer_connection(checkpointer)
348
+ _CHECKPOINTERS.clear()
349
+ _ASYNC_CHECKPOINTERS.clear()
350
+
351
+
352
+ async def clear_langgraph_checkpointer_cache_async() -> None:
353
+ for checkpointer in _CHECKPOINTERS.values():
354
+ _close_checkpointer_connection(checkpointer)
355
+ for checkpointer in _ASYNC_CHECKPOINTERS.values():
356
+ await _aclose_checkpointer_connection(checkpointer)
357
+ _CHECKPOINTERS.clear()
358
+ _ASYNC_CHECKPOINTERS.clear()
359
+
360
+
361
+ def ensure_langgraph_checkpointer(
362
+ sessions_dir: str,
363
+ session_name: str = "default",
364
+ ) -> Any:
365
+ if _should_use_sqlite_checkpointer(sessions_dir, session_name=session_name):
366
+ sqlite_path = _sqlite_checkpointer_path(sessions_dir, session_name=session_name)
367
+ cached = _CHECKPOINTERS.get(sqlite_path)
368
+ if cached is not None:
369
+ return cached
370
+ checkpointer = SqliteSaver(sqlite3.connect(sqlite_path, check_same_thread=False))
371
+ _CHECKPOINTERS[sqlite_path] = checkpointer
372
+ return checkpointer
373
+
374
+ base_path = _legacy_checkpointer_base_path(sessions_dir, session_name=session_name)
375
+ cached = _CHECKPOINTERS.get(base_path)
376
+ if cached is not None:
377
+ return cached
378
+ checkpointer = PersistentInMemoryCheckpointer(base_path)
379
+ _CHECKPOINTERS[base_path] = checkpointer
380
+ return checkpointer
381
+
382
+
383
+ async def ensure_langgraph_checkpointer_async(
384
+ sessions_dir: str,
385
+ session_name: str = "default",
386
+ ) -> Any:
387
+ if _should_use_sqlite_checkpointer(sessions_dir, session_name=session_name) and AsyncSqliteSaver is not None and aiosqlite is not None:
388
+ sqlite_path = _sqlite_checkpointer_path(sessions_dir, session_name=session_name)
389
+ cached = _ASYNC_CHECKPOINTERS.get(sqlite_path)
390
+ if cached is not None:
391
+ return cached
392
+ connection = await aiosqlite.connect(sqlite_path)
393
+ checkpointer = AsyncSqliteSaver(connection)
394
+ _ASYNC_CHECKPOINTERS[sqlite_path] = checkpointer
395
+ return checkpointer
396
+ return ensure_langgraph_checkpointer(sessions_dir, session_name=session_name)
397
+
398
+
399
+ def has_thread_checkpoint(
400
+ sessions_dir: str,
401
+ agent_name: str,
402
+ session_name: str = "default",
403
+ *,
404
+ thread_id: str | None = None,
405
+ ) -> bool:
406
+ checkpointer = ensure_langgraph_checkpointer(sessions_dir, session_name=session_name)
407
+ return checkpointer.get_tuple(
408
+ build_thread_config(agent_name, session_name=session_name, thread_id=thread_id)
409
+ ) is not None
410
+
411
+
412
+ async def has_thread_checkpoint_async(
413
+ sessions_dir: str,
414
+ agent_name: str,
415
+ session_name: str = "default",
416
+ *,
417
+ thread_id: str | None = None,
418
+ ) -> bool:
419
+ checkpointer = await ensure_langgraph_checkpointer_async(sessions_dir, session_name=session_name)
420
+ config = build_thread_config(agent_name, session_name=session_name, thread_id=thread_id)
421
+ aget_tuple = getattr(checkpointer, "aget_tuple", None)
422
+ if aget_tuple is not None:
423
+ return await aget_tuple(config) is not None
424
+ return checkpointer.get_tuple(config) is not None
425
+
426
+
427
+ def delete_langgraph_thread(
428
+ sessions_dir: str,
429
+ thread_id: str,
430
+ session_name: str = "default",
431
+ ) -> bool:
432
+ checkpointer = ensure_langgraph_checkpointer(sessions_dir, session_name=session_name)
433
+ delete_thread = getattr(checkpointer, "delete_thread", None)
434
+ if delete_thread is None:
435
+ return False
436
+ delete_thread(thread_id)
437
+ return True
438
+
439
+
440
+ async def delete_langgraph_thread_async(
441
+ sessions_dir: str,
442
+ thread_id: str,
443
+ session_name: str = "default",
444
+ ) -> bool:
445
+ checkpointer = await ensure_langgraph_checkpointer_async(sessions_dir, session_name=session_name)
446
+ adelete_thread = getattr(checkpointer, "adelete_thread", None)
447
+ if adelete_thread is not None:
448
+ await adelete_thread(thread_id)
449
+ return True
450
+ delete_thread = getattr(checkpointer, "delete_thread", None)
451
+ if delete_thread is None:
452
+ return False
453
+ delete_thread(thread_id)
454
+ return True
455
+
456
+
457
+ def _should_use_sqlite_checkpointer(sessions_dir: str, session_name: str = "default") -> bool:
458
+ if SqliteSaver is None:
459
+ return False
460
+ sqlite_path = _sqlite_checkpointer_path(sessions_dir, session_name=session_name)
461
+ if sqlite_path.exists():
462
+ return True
463
+ return not _has_legacy_checkpoint_artifacts(_legacy_checkpointer_base_path(sessions_dir, session_name=session_name))
464
+
465
+
466
+ def _legacy_checkpointer_base_path(sessions_dir: str, session_name: str = "default") -> Path:
467
+ return Path(sessions_dir).expanduser() / f"{session_name}.langgraph"
468
+
469
+
470
+ def _sqlite_checkpointer_path(sessions_dir: str, session_name: str = "default") -> Path:
471
+ base_dir = Path(sessions_dir).expanduser()
472
+ base_dir.mkdir(parents=True, exist_ok=True)
473
+ return base_dir / f"{session_name}.langgraph.sqlite3"
474
+
475
+
476
+ def _has_legacy_checkpoint_artifacts(base_path: Path) -> bool:
477
+ return any(
478
+ artifact.exists()
479
+ for artifact in (
480
+ base_path.with_suffix(".storage.pkl"),
481
+ base_path.with_suffix(".writes.pkl"),
482
+ base_path.with_suffix(".blobs.pkl"),
483
+ )
484
+ )
485
+
486
+
487
+ def _close_checkpointer_connection(checkpointer: Any) -> None:
488
+ connection = getattr(checkpointer, "connection", None) or getattr(checkpointer, "conn", None)
489
+ if connection is None:
490
+ return
491
+ try:
492
+ result = connection.close()
493
+ if inspect.isawaitable(result):
494
+ try:
495
+ import asyncio
496
+
497
+ asyncio.run(result)
498
+ except Exception:
499
+ pass
500
+ except Exception:
501
+ pass
502
+
503
+
504
+ async def _aclose_checkpointer_connection(checkpointer: Any) -> None:
505
+ connection = getattr(checkpointer, "connection", None) or getattr(checkpointer, "conn", None)
506
+ if connection is None:
507
+ return
508
+ try:
509
+ result = connection.close()
510
+ if inspect.isawaitable(result):
511
+ await result
512
+ except Exception:
513
+ pass
@@ -0,0 +1 @@
1
+ """MCP configuration and client helpers."""
agencli/mcp/client.py ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agencli.mcp.config import load_mcp_servers
6
+
7
+
8
+ class MCPToolClient:
9
+ """Thin wrapper around MultiServerMCPClient for AgenCLI."""
10
+
11
+ def __init__(self, mcp_config_path: str) -> None:
12
+ self.mcp_config_path = mcp_config_path
13
+
14
+ def load_server_map(self) -> dict[str, dict[str, Any]]:
15
+ servers = load_mcp_servers(self.mcp_config_path)
16
+ return {name: server.to_client_dict() for name, server in servers.items()}
17
+
18
+ async def get_tools(self, server_names: list[str] | None = None) -> list[Any]:
19
+ try:
20
+ from langchain_mcp_adapters.client import MultiServerMCPClient
21
+ except ImportError as exc:
22
+ raise RuntimeError(
23
+ "langchain-mcp-adapters is not installed yet. Run `uv sync` after dependencies are added."
24
+ ) from exc
25
+
26
+ server_map = self.load_server_map()
27
+ if server_names is not None:
28
+ server_map = {name: server_map[name] for name in server_names if name in server_map}
29
+ if not server_map:
30
+ return []
31
+
32
+ client = MultiServerMCPClient(server_map)
33
+ return await client.get_tools()
agencli/mcp/config.py ADDED
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import shutil
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class MCPServerConfig:
13
+ transport: str
14
+ command: str | None = None
15
+ args: list[str] = field(default_factory=list)
16
+ url: str | None = None
17
+ env: dict[str, str] = field(default_factory=dict)
18
+
19
+ @classmethod
20
+ def from_dict(cls, raw: dict[str, Any]) -> "MCPServerConfig":
21
+ return cls(
22
+ transport=raw["transport"],
23
+ command=raw.get("command"),
24
+ args=list(raw.get("args", [])),
25
+ url=raw.get("url"),
26
+ env=dict(raw.get("env", {})),
27
+ )
28
+
29
+ def to_client_dict(self) -> dict[str, Any]:
30
+ payload: dict[str, Any] = {"transport": self.transport}
31
+ if self.command:
32
+ payload["command"] = _resolve_command(self.command)
33
+ if self.args:
34
+ payload["args"] = [_resolve_template(arg) for arg in self.args]
35
+ if self.url:
36
+ payload["url"] = _resolve_template(self.url)
37
+ if self.env:
38
+ payload["env"] = {name: _resolve_template(value) for name, value in self.env.items()}
39
+ return payload
40
+
41
+
42
+ def _resolve_template(value: str) -> str:
43
+ for key, env_value in os.environ.items():
44
+ value = value.replace(f"{{{key}}}", env_value)
45
+ return value
46
+
47
+
48
+ def _resolve_command(command: str) -> str:
49
+ if os.name != "nt":
50
+ return command
51
+ if shutil.which(command):
52
+ return command
53
+
54
+ for suffix in (".cmd", ".exe", ".bat"):
55
+ candidate = command if command.lower().endswith(suffix) else f"{command}{suffix}"
56
+ if shutil.which(candidate):
57
+ return candidate
58
+ return command
59
+
60
+
61
+ def load_mcp_servers(mcp_config_path: str) -> dict[str, MCPServerConfig]:
62
+ path = Path(mcp_config_path).expanduser()
63
+ if not path.exists():
64
+ return {}
65
+
66
+ raw = json.loads(path.read_text(encoding="utf-8"))
67
+ if "mcpServers" in raw:
68
+ raw = raw["mcpServers"]
69
+ return {name: MCPServerConfig.from_dict(server) for name, server in raw.items()}
70
+
71
+
72
+ def save_mcp_servers(mcp_config_path: str, servers: dict[str, MCPServerConfig]) -> Path:
73
+ path = Path(mcp_config_path).expanduser()
74
+ path.parent.mkdir(parents=True, exist_ok=True)
75
+ payload = {"mcpServers": {name: asdict(server) for name, server in servers.items()}}
76
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
77
+ return path
78
+
79
+
80
+ def upsert_mcp_server(mcp_config_path: str, name: str, server: MCPServerConfig) -> Path:
81
+ servers = load_mcp_servers(mcp_config_path)
82
+ servers[name] = server
83
+ return save_mcp_servers(mcp_config_path, servers)
84
+
85
+
86
+ def default_mcp_servers(workspace_dir: str) -> dict[str, MCPServerConfig]:
87
+ return {
88
+ "search": MCPServerConfig(
89
+ transport="http",
90
+ url="https://mcp.tavily.com/mcp/?tavilyApiKey={TAVILY_API_KEY}",
91
+ ),
92
+ }
93
+
94
+
95
+ def bootstrap_default_mcp_servers(mcp_config_path: str, workspace_dir: str) -> Path:
96
+ servers = load_mcp_servers(mcp_config_path)
97
+ for name, server in default_mcp_servers(workspace_dir).items():
98
+ servers.setdefault(name, server)
99
+ return save_mcp_servers(mcp_config_path, servers)
@@ -0,0 +1 @@
1
+ """Model provider utilities."""