mcp prototype

frdel committed Jun 3, 2025 at 22:25 UTC 5126dc9732bc9a1a907b45ca165ae77bf75d2e36
9 files changed +230 -79
docker/base/fs/ins/install_python.sh
+1 -1
@@ -51,6 +51,6 @@ pip install torch --index-url https://download.pytorch.org/whl/cpu
51
52 echo "====================PYTHON UV ===================="
53
54 -curl -Ls https://astral.sh/uv/install.sh | sh
54 +curl -Ls https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh
55
56 echo "====================PYTHON END===================="
docker/run/fs/ins/post_install.sh
+1 -1
@@ -2,4 +2,4 @@
2
3 # Cleanup package list
4 rm -rf /var/lib/apt/lists/*
5 -apt-get clean
\ No newline at end of file
5 +# apt-get clean
\ No newline at end of file
python/api/mcp_servers_apply.py
+5 -3
@@ -3,15 +3,17 @@ from flask import Request, Response
3
4 from typing import Any
5
6 -from python.helpers.mcp_handler import MCPConfig
6 +# from python.helpers.mcp_handler import MCPConfig
7 +from python.helpers.settings import set_settings_delta
8
9
10 class McpServersApply(ApiHandler):
11 async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
12 mcp_servers = input["mcp_servers"]
12 - mcp_config = MCPConfig.get_instance()
13 try:
14 - MCPConfig.update(mcp_servers)
14 + # MCPConfig.update(mcp_servers) # done in settings automatically
15 + set_settings_delta({"mcp_servers": mcp_servers})
16 +
17 except Exception as e:
18 return {"success": False, "error": str(e)}
19 return {"success": True}
python/helpers/mcp_handler.py
+32 -23
@@ -167,24 +167,28 @@ class MCPTool(Tool):
167 user_message_text[:max_user_context_len] + "... (truncated)"
168 )
169
170 - contextual_block = f"""
171 -\n--- End of Results for MCP Tool: {self.name} ---
170 +# commented out for now, output should be unified between tools and MCPs
171
173 -**Original Tool Call Details:**
174 -* **Tool:** `{self.name}`
175 -* **Arguments Given:**
176 - ```json
177 -{json.dumps(self.args, indent=2)}
178 - ```
172 +# contextual_block = f"""
173 +# \n--- End of Results for MCP Tool: {self.name} ---
174
180 -**Related User Request Context:**
181 -{user_message_text}
175 +# **Original Tool Call Details:**
176 +# * **Tool:** `{self.name}`
177 +# * **Arguments Given:**
178 +# ```json
179 +# {json.dumps(self.args, indent=2)}
180 +# ```
181
183 -**Next Steps Reminder for {self.name}:**
184 -If this action is part of an ongoing sequence, consider the next step with this tool or another appropriate tool. If the sequence is complete or this was a one-off action, analyze the final output and report to the user or proceed with the overall plan.
185 -"""
182 +# **Related User Request Context:**
183 +# {user_message_text}
184
187 - final_text_for_agent = raw_tool_response + contextual_block
185 +# **Next Steps Reminder for {self.name}:**
186 +# If this action is part of an ongoing sequence, consider the next step with this tool or another appropriate tool. If the sequence is complete or this was a one-off action, analyze the final output and report to the user or proceed with the overall plan.
187 +# """
188 +
189 +# final_text_for_agent = raw_tool_response + contextual_block
190 +
191 + final_text_for_agent = raw_tool_response
192
193 self.agent.hist_add_tool_result(self.name, final_text_for_agent)
194 (
@@ -211,8 +215,8 @@ class MCPServerRemote(BaseModel):
215 description: Optional[str] = Field(default="Remote SSE Server")
216 url: str = Field(default_factory=str)
217 headers: dict[str, Any] | None = Field(default_factory=dict[str, Any])
214 - timeout: float = Field(default=5.0)
215 - sse_read_timeout: float = Field(default=60.0 * 5.0)
218 + init_timeout: int = Field(default=0)
219 + tool_timeout: int = Field(default=0)
220 disabled: bool = Field(default=False)
221
222 __lock: ClassVar[threading.Lock] = PrivateAttr(default=threading.Lock())
@@ -257,8 +261,8 @@ class MCPServerRemote(BaseModel):
261 "description",
262 "url",
263 "headers",
260 - "timeout",
261 - "sse_read_timeout",
264 + "init_timeout",
265 + "tool_timeout",
266 "disabled",
267 ]:
268 if key == "name":
@@ -282,6 +286,8 @@ class MCPServerLocal(BaseModel):
286 encoding_error_handler: Literal["strict", "ignore", "replace"] = Field(
287 default="strict"
288 )
289 + init_timeout: int = Field(default=0)
290 + tool_timeout: int = Field(default=0)
291 disabled: bool = Field(default=False)
292
293 __lock: ClassVar[threading.Lock] = PrivateAttr(default=threading.Lock())
@@ -329,6 +335,8 @@ class MCPServerLocal(BaseModel):
335 "env",
336 "encoding",
337 "encoding_error_handler",
338 + "init_timeout",
339 + "tool_timeout",
340 "disabled",
341 ]:
342 if key == "name":
@@ -695,9 +703,9 @@ class MCPConfig(BaseModel):
703 f"#### Usage:\n"
704 f"~~~json\n"
705 f"{{\n"
698 - f' "observations": ["..."],\n'
706 + # f' "observations": ["..."],\n' # TODO: this should be a prompt file with placeholders
707 f' "thoughts": ["..."],\n'
700 - f' "reflection": ["..."],\n'
708 + # f' "reflection": ["..."],\n' # TODO: this should be a prompt file with placeholders
709 f" \"tool_name\": \"{server_name}.{tool['name']}\",\n"
710 f' "tool_args": {{\n'
711 f"{tool_args}"
@@ -848,7 +856,7 @@ class MCPClientBase(ABC):
856
857 try:
858 set = settings.get_settings()
851 - await self._execute_with_session(list_tools_op, read_timeout_seconds=set["mcp_client_init_timeout"])
859 + await self._execute_with_session(list_tools_op, read_timeout_seconds=self.server.init_timeout or set["mcp_client_init_timeout"])
860 except Exception as e:
861 # e = eg.exceptions[0]
862 error_text = errors.format_error(e, 0, 0)
@@ -989,12 +997,13 @@ class MCPClientRemote(MCPClientBase):
997 ]:
998 """Connect to an MCP server, init client and save stdio/write streams"""
999 server: MCPServerRemote = cast(MCPServerRemote, self.server)
1000 + set = settings.get_settings()
1001 stdio_transport = await current_exit_stack.enter_async_context(
1002 sse_client(
1003 url=server.url,
1004 headers=server.headers,
996 - timeout=server.timeout,
997 - sse_read_timeout=server.sse_read_timeout,
1005 + timeout=server.init_timeout or set["mcp_client_init_timeout"],
1006 + sse_read_timeout=server.tool_timeout or set["mcp_client_tool_timeout"],
1007 )
1008 )
1009 return stdio_transport
python/helpers/settings.py
+53 -50
@@ -751,7 +751,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
751 mcp_server_section: SettingsSection = {
752 "id": "mcp_server",
753 "title": "A0 MCP Server",
754 - "description": "Agent Zero can be exposed as an SSE MCP server. It can then be accessed by MCP clients on the URL and port of the web UI + /mcp/sse, for example http://localhost:5000/mcp/sse. The same applies to public URL using Cloudflare Tunnel.",
754 + "description": "Agent Zero can be exposed as an SSE MCP server. See <a href=\"javascript:openModal('settings/mcp/server/example.html')\">connection example</a>.",
755 "fields": mcp_server_fields,
756 "tab": "mcp",
757 }
@@ -811,18 +811,19 @@ def get_settings() -> Settings:
811 return norm
812
813
814 -def set_settings(settings: Settings):
814 +def set_settings(settings: Settings, apply: bool = True):
815 global _settings
816 previous = _settings
817 _settings = normalize_settings(settings)
818 _write_settings_file(_settings)
819 - _apply_settings(previous)
819 + if apply:
820 + _apply_settings(previous)
821
822
822 -def set_settings_delta(delta: dict):
823 +def set_settings_delta(delta: dict, apply: bool = True):
824 current = get_settings()
825 new = {**current, **delta}
825 - set_settings(new) # type: ignore
826 + set_settings(new, apply) # type: ignore
827
828
829 def normalize_settings(settings: Settings) -> Settings:
@@ -957,12 +958,13 @@ def _apply_settings(previous: Settings | None):
958 agent = agent.get_data(agent.DATA_NAME_SUBORDINATE)
959
960 # reload whisper model if necessary
960 - task = defer.DeferredTask().start_task(
961 - whisper.preload, _settings["stt_model_size"]
962 - ) # TODO overkill, replace with background task
961 + if not previous or _settings["stt_model_size"] != previous["stt_model_size"]:
962 + task = defer.DeferredTask().start_task(
963 + whisper.preload, _settings["stt_model_size"]
964 + ) # TODO overkill, replace with background task
965
966 # force memory reload on embedding model change
965 - if previous and (
967 + if not previous or (
968 _settings["embed_model_name"] != previous["embed_model_name"]
969 or _settings["embed_model_provider"] != previous["embed_model_provider"]
970 or _settings["embed_model_kwargs"] != previous["embed_model_kwargs"]
@@ -972,55 +974,56 @@ def _apply_settings(previous: Settings | None):
974 memory_reload()
975
976 # update mcp settings if necessary
975 - from python.helpers.mcp_handler import MCPConfig
976 -
977 - async def update_mcp_settings(mcp_servers: str):
978 - PrintStyle(
979 - background_color="black", font_color="white", padding=True
980 - ).print("Updating MCP config...")
981 - first_context = AgentContext.first()
982 - if first_context:
983 - first_context.log.log(
984 - type="info", content="Updating MCP settings...", temp=True
985 - )
977 + if not previous or _settings["mcp_servers"] != previous["mcp_servers"]:
978 + from python.helpers.mcp_handler import MCPConfig
979
987 - mcp_config = MCPConfig.get_instance()
988 - try:
989 - MCPConfig.update(mcp_servers)
990 - except Exception as e:
980 + async def update_mcp_settings(mcp_servers: str):
981 + PrintStyle(
982 + background_color="black", font_color="white", padding=True
983 + ).print("Updating MCP config...")
984 + first_context = AgentContext.first()
985 if first_context:
986 first_context.log.log(
993 - type="warning",
994 - content=f"Failed to update MCP settings: {e}",
995 - temp=False,
987 + type="info", content="Updating MCP settings...", temp=True
988 + )
989 +
990 + mcp_config = MCPConfig.get_instance()
991 + try:
992 + MCPConfig.update(mcp_servers)
993 + except Exception as e:
994 + if first_context:
995 + first_context.log.log(
996 + type="warning",
997 + content=f"Failed to update MCP settings: {e}",
998 + temp=False,
999 + )
1000 + (
1001 + PrintStyle(
1002 + background_color="red", font_color="black", padding=True
1003 + ).print("Failed to update MCP settings")
1004 + )
1005 + (
1006 + PrintStyle(
1007 + background_color="black", font_color="red", padding=True
1008 + ).print(f"{e}")
1009 )
997 - (
998 - PrintStyle(
999 - background_color="red", font_color="black", padding=True
1000 - ).print("Failed to update MCP settings")
1001 - )
1002 - (
1003 - PrintStyle(
1004 - background_color="black", font_color="red", padding=True
1005 - ).print(f"{e}")
1006 - )
1010
1008 - PrintStyle(
1009 - background_color="#6734C3", font_color="white", padding=True
1010 - ).print("Parsed MCP config:")
1011 - (
1011 PrintStyle(
1013 - background_color="#334455", font_color="white", padding=False
1014 - ).print(mcp_config.model_dump_json())
1015 - )
1016 - if first_context:
1017 - first_context.log.log(
1018 - type="info", content="Finished updating MCP settings :)", temp=True
1012 + background_color="#6734C3", font_color="white", padding=True
1013 + ).print("Parsed MCP config:")
1014 + (
1015 + PrintStyle(
1016 + background_color="#334455", font_color="white", padding=False
1017 + ).print(mcp_config.model_dump_json())
1018 )
1019 + if first_context:
1020 + first_context.log.log(
1021 + type="info", content="Finished updating MCP settings :)", temp=True
1022 + )
1023
1021 - task2 = defer.DeferredTask().start_task(
1022 - update_mcp_settings, config.mcp_servers
1023 - ) # TODO overkill, replace with background task
1024 + task2 = defer.DeferredTask().start_task(
1025 + update_mcp_settings, config.mcp_servers
1026 + ) # TODO overkill, replace with background task
1027
1028
1029 def _env_to_dict(data: str):
webui/components/settings/mcp/client/example.html new
+80
@@ -0,0 +1,80 @@
1 +<html>
2 +
3 +<head>
4 + <title>Configuring MCP Servers</title>
5 +
6 +</head>
7 +
8 +<body>
9 + <div x-data>
10 + <p>Agent Zero uses standard JSON configuration known from other AI applications.<br>
11 + The configuration is a JSON object containing "mcpServers" object where each key is an individual MCP
12 + server.<br>
13 + Local servers are defined by a "command", "args", "env" variables.<br>
14 + Remote servers are defined by a "url", "headers".<br>
15 + "disabled" can be set to true to disable a server without removing config.<br>
16 + All servers can also define "init_timeout" and "tool_timeout" which override global settings.</p>
17 +
18 +
19 + <h3>Example MCP Servers Configuration JSON</h3>
20 + <div id="mcp-servers-example"></div>
21 +
22 + <script>
23 + setTimeout(() => {
24 + const url = window.location.origin;
25 + const jsonExample = JSON.stringify({
26 + "mcpServers":
27 + {
28 + "sqlite": {
29 + "command": "uvx",
30 + "args": [
31 + "mcp-server-sqlite",
32 + "--db-path",
33 + "/root/db.sqlite"
34 + ],
35 + "init_timeout": 10,
36 + "tool_timeout": 200
37 + },
38 + "sequential-thinking": {
39 + "disabled": true,
40 + "command": "npx",
41 + "args": [
42 + "--yes",
43 + "--package",
44 + "@modelcontextprotocol/server-sequential-thinking",
45 + "mcp-server-sequential-thinking"
46 + ]
47 + },
48 + "agent-zero": {
49 + "type": "sse",
50 + "serverUrl": `${url}/mcp/sse`
51 + }
52 + }
53 + }, null, 2);
54 +
55 + const editor = ace.edit("mcp-servers-example");
56 + const dark = localStorage.getItem("darkMode");
57 + if (dark != "false") {
58 + editor.setTheme("ace/theme/github_dark");
59 + } else {
60 + editor.setTheme("ace/theme/tomorrow");
61 + }
62 + editor.session.setMode("ace/mode/json");
63 + editor.setValue(jsonExample);
64 + editor.clearSelection();
65 + editor.setReadOnly(true);
66 + }, 0);
67 + </script>
68 + <!-- </template> -->
69 + </div>
70 +
71 + <style>
72 + #mcp-servers-example {
73 + width: 100%;
74 + height: 40em;
75 + }
76 + </style>
77 +
78 +</body>
79 +
80 +</html>
\ No newline at end of file
webui/components/settings/mcp/client/mcp-servers-store.js
+3 -1
@@ -83,6 +83,7 @@ const model = {
83 const resp = await API.callJsonApi("mcp_servers_status", null);
84 if (resp.success) {
85 this.servers = resp.status;
86 + this.servers.sort((a, b) => a.name.localeCompare(b.name));
87 }
88 },
89
@@ -98,7 +99,8 @@ const model = {
99 await API.callJsonApi("mcp_servers_apply", {
100 mcp_servers: this.getEditorValue(),
101 });
101 - scrollModal("mcp-servers-status");
102 + await sleep(5000); // just to prevent user from clicking apply multiple times
103 + // scrollModal("mcp-servers-status");
104 } catch (error) {
105 console.error("Failed to apply MCP servers:", error);
106 alert("Failed to apply MCP servers: " + error.message);
webui/components/settings/mcp/client/mcp-servers.html
+1
@@ -14,6 +14,7 @@
14 <div x-init="$store.mcpServersStore.initialize()" x-destroy="$store.mcpServersStore.onClose()">
15
16 <h3>MCP Servers Configuration JSON
17 + <button class="btn slim" style="margin-left: 0.5em;" onclick="openModal('settings/mcp/client/example.html')">Examples</button>
18 <button class="btn slim" style="margin-left: 0.5em;"
19 @click="$store.mcpServersStore.formatJson()">Reformat</button>
20 <button class="btn slim primary" :disabled="$store.mcpServersStore.loading"
webui/components/settings/mcp/server/example.html new
+54
@@ -0,0 +1,54 @@
1 +<html>
2 +
3 +<head>
4 + <title>Connection to A0 MCP Server</title>
5 +
6 +</head>
7 +
8 +<body>
9 + <div x-data>
10 + <p>Agent Zero MCP Server is an SSE MCP running on the same URL and port as the Web UI + /mcp/sse path.</p>
11 + <p>The same applies if you run A0 on a public URL using a tunnel.</p>
12 +
13 + <h3>Example MCP Server Configuration JSON</h3>
14 + <div id="mcp-server-example"></div>
15 +
16 + <script>
17 + setTimeout(() => {
18 + const url = window.location.origin;
19 + const jsonExample = JSON.stringify({
20 + "mcpServers":
21 + {
22 + "agent-zero": {
23 + "type": "sse",
24 + "serverUrl": `${url}/mcp/sse`
25 + }
26 + }
27 + }, null, 2);
28 +
29 + const editor = ace.edit("mcp-server-example");
30 + const dark = localStorage.getItem("darkMode");
31 + if (dark != "false") {
32 + editor.setTheme("ace/theme/github_dark");
33 + } else {
34 + editor.setTheme("ace/theme/tomorrow");
35 + }
36 + editor.session.setMode("ace/mode/json");
37 + editor.setValue(jsonExample);
38 + editor.clearSelection();
39 + editor.setReadOnly(true);
40 + }, 0);
41 + </script>
42 + <!-- </template> -->
43 + </div>
44 +
45 + <style>
46 + #mcp-server-example {
47 + width: 100%;
48 + height: 15em;
49 + }
50 + </style>
51 +
52 +</body>
53 +
54 +</html>
\ No newline at end of file