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