Skip to content

Commit a3bf099

Browse files
committed
Add dynamic loading of user-configured custom MCP servers and update documentation to reflect new capabilities
1 parent e31ea38 commit a3bf099

3 files changed

Lines changed: 85 additions & 1 deletion

File tree

kady_agent/instructions/main_agent.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Choose the lightest reliable path:
2929

3030
- Prefer Parallel Search MCP (`web_search`, `web_fetch`) for open-web search and URL content retrieval.
3131
- Prefer Docling for document conversion, text extraction, and markdown export.
32+
- Users may install custom MCP tools (e.g. memory/knowledge-graph, filesystem, databases, specialized APIs) via the Settings panel. These tools appear alongside the built-in ones — use them directly whenever the request matches their capabilities instead of routing through `delegate_task`.
3233
- For reports, papers, literature reviews, or other structured prose, instruct the expert to use the `writing` skill.
3334

3435
## After tool use

kady_agent/mcps.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import os
34
from typing import List, Optional
@@ -12,6 +13,8 @@
1213
StreamableHTTPConnectionParams,
1314
)
1415

16+
from .gemini_settings import load_custom_mcps
17+
1518
logger = logging.getLogger(__name__)
1619

1720

@@ -43,6 +46,83 @@ async def close(self) -> None:
4346
pass
4447

4548

49+
def _make_toolset(name: str, spec: dict) -> ResilientMcpToolset | None:
50+
"""Create a ResilientMcpToolset from a custom MCP server spec.
51+
52+
Supports two formats (matching the Gemini CLI settings schema):
53+
- HTTP: ``{"httpUrl": "...", "headers": {...}}``
54+
- Stdio: ``{"command": "...", "args": [...], "env": {...}}``
55+
"""
56+
if "httpUrl" in spec:
57+
params = StreamableHTTPConnectionParams(
58+
url=spec["httpUrl"],
59+
headers=spec.get("headers", {}),
60+
timeout=spec.get("timeout", 120),
61+
)
62+
elif "command" in spec:
63+
params = StdioConnectionParams(
64+
server_params=StdioServerParameters(
65+
command=spec["command"],
66+
args=spec.get("args", []),
67+
env=spec.get("env"),
68+
),
69+
timeout=float(spec.get("timeout", 120)),
70+
)
71+
else:
72+
logger.warning("Skipping custom MCP %r: no 'command' or 'httpUrl' key", name)
73+
return None
74+
return ResilientMcpToolset(McpToolset(connection_params=params), label=name)
75+
76+
77+
class DynamicCustomMcpToolset(BaseToolset):
78+
"""Reads ``user_config/custom_mcps.json`` on every agent turn and
79+
lazily creates / caches MCP connections for each entry.
80+
81+
When the config file changes between turns the stale connections are
82+
torn down and new ones are established automatically — no server
83+
restart required.
84+
"""
85+
86+
def __init__(self) -> None:
87+
super().__init__()
88+
self._toolsets: dict[str, ResilientMcpToolset] = {}
89+
self._config_hash: str | None = None
90+
91+
async def get_tools(
92+
self, readonly_context: Optional[ReadonlyContext] = None
93+
) -> List[BaseTool]:
94+
config = load_custom_mcps()
95+
new_hash = json.dumps(config, sort_keys=True)
96+
97+
if new_hash != self._config_hash:
98+
await self._rebuild(config)
99+
self._config_hash = new_hash
100+
101+
all_tools: List[BaseTool] = []
102+
for ts in self._toolsets.values():
103+
all_tools.extend(await ts.get_tools(readonly_context))
104+
return all_tools
105+
106+
async def _rebuild(self, config: dict) -> None:
107+
for ts in self._toolsets.values():
108+
await ts.close()
109+
self._toolsets.clear()
110+
111+
for name, spec in config.items():
112+
ts = _make_toolset(name, spec)
113+
if ts is not None:
114+
self._toolsets[name] = ts
115+
116+
async def close(self) -> None:
117+
for ts in self._toolsets.values():
118+
await ts.close()
119+
self._toolsets.clear()
120+
121+
122+
# ---------------------------------------------------------------------------
123+
# Built-in MCP servers
124+
# ---------------------------------------------------------------------------
125+
46126
all_mcps: list[BaseToolset] = []
47127

48128
if os.getenv("PARALLEL_API_KEY"):
@@ -71,3 +151,6 @@ async def close(self) -> None:
71151
label="Docling MCP",
72152
)
73153
all_mcps.append(docling_mcp)
154+
155+
# User-configured custom MCP servers (loaded dynamically per-request)
156+
all_mcps.append(DynamicCustomMcpToolset())

web/src/components/settings-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function McpServersPanel() {
8686

8787
<div className="flex items-center justify-between">
8888
<p className="text-[11px] text-muted-foreground">
89-
Changes apply to the next Gemini CLI invocation.
89+
Changes apply to the next message.
9090
</p>
9191
<Button
9292
size="sm"

0 commit comments

Comments
 (0)