MCP client merge
frdel committed
May 30, 2025 at 21:53 UTC
922a4a71d5709ace6ab57fbb645ad34e1ee60c44
4 files changed
+202
-185
docker/base/fs/ins/install_base_packages.sh
+4
@@ -6,4 +6,8 @@ echo "====================BASE PACKAGES START===================="
6
apt-get install -y --no-install-recommends \
7
nodejs npm openssh-server sudo curl wget git ffmpeg supervisor cron
8
9
+echo "====================BASE PACKAGES NPM===================="
10
+
11
+npm i -g npx shx
12
+
13
echo "====================BASE PACKAGES END===================="
initialize.py
+161
-161
@@ -8,182 +8,182 @@ import shutil
8
from python.helpers.print_style import PrintStyle
9
10
11
-_NPM_CHECKS_DONE = False
11
+# _NPM_CHECKS_DONE = False
12
13
14
-# Helper function to ensure an MCP package is globally installed
15
-def _ensure_mcp_package_globally_installed(package_name: str, executable_name: str):
16
- PrintStyle(background_color="blue", font_color="white", padding=True).print(
17
- f"Attempting to ensure MCP server executable '{executable_name}' (from package '{package_name}') is available..."
18
- )
19
- if shutil.which("npm"):
20
- if not shutil.which(executable_name):
21
- PrintStyle(font_color="yellow", padding=True).print(
22
- f"'{executable_name}' not found in PATH. Attempting global npm install of '{package_name}'..."
23
- )
24
- try:
25
- npm_command = ["npm", "i", "-g", package_name, "--no-fund", "--no-audit"]
26
- process = subprocess.run(npm_command, capture_output=True, text=True, check=False)
27
- if process.returncode == 0:
28
- PrintStyle(font_color="green", padding=True).print(
29
- f"Successfully installed '{package_name}' globally via npm."
30
- )
31
- if shutil.which(executable_name):
32
- PrintStyle(font_color="green", padding=True).print(
33
- f"'{executable_name}' is now available in PATH after install."
34
- )
35
- return True # Successfully installed and found
36
- else:
37
- PrintStyle(font_color="orange", padding=True).print(
38
- f"WARNING: npm install of '{package_name}' reported success, but '{executable_name}' still not found in PATH. " +
39
- "The 'npx' command in settings.json might still be necessary or there might be an issue with PATH."
40
- )
41
- return False # Install reported success, but executable not found
42
- else:
43
- PrintStyle(font_color="red", padding=True).print(
44
- f"Failed to install '{package_name}' globally via npm. Return code: {process.returncode}"
45
- )
46
- PrintStyle(font_color="red", padding=False).print(f"npm stdout: {process.stdout.strip()}")
47
- PrintStyle(font_color="red", padding=False).print(f"npm stderr: {process.stderr.strip()}")
48
- return False # Install failed
49
- except FileNotFoundError:
50
- PrintStyle(font_color="red", padding=True).print(
51
- f"ERROR: 'npm' command not found. Cannot attempt to install '{package_name}'."
52
- )
53
- return False # npm not found
54
- except Exception as e:
55
- PrintStyle(font_color="red", padding=True).print(
56
- f"Exception during npm install of '{package_name}': {e}"
57
- )
58
- return False # Other exception during install
59
- else:
60
- PrintStyle(font_color="green", padding=True).print(
61
- f"'{executable_name}' (from package '{package_name}') already found in PATH."
62
- )
63
- return True # Already found in PATH
64
- else:
65
- PrintStyle(font_color="red", padding=True).print(
66
- f"ERROR: 'npm' command not found. Cannot check for or install '{executable_name}' from '{package_name}'."
67
- )
68
- return False # npm command not found, cannot check or install
69
- # Fallback, though logic above should cover all paths to return explicitly
70
- return False
14
+# # Helper function to ensure an MCP package is globally installed
15
+# def _ensure_mcp_package_globally_installed(package_name: str, executable_name: str):
16
+# PrintStyle(background_color="blue", font_color="white", padding=True).print(
17
+# f"Attempting to ensure MCP server executable '{executable_name}' (from package '{package_name}') is available..."
18
+# )
19
+# if shutil.which("npm"):
20
+# if not shutil.which(executable_name):
21
+# PrintStyle(font_color="yellow", padding=True).print(
22
+# f"'{executable_name}' not found in PATH. Attempting global npm install of '{package_name}'..."
23
+# )
24
+# try:
25
+# npm_command = ["npm", "i", "-g", package_name, "--no-fund", "--no-audit"]
26
+# process = subprocess.run(npm_command, capture_output=True, text=True, check=False)
27
+# if process.returncode == 0:
28
+# PrintStyle(font_color="green", padding=True).print(
29
+# f"Successfully installed '{package_name}' globally via npm."
30
+# )
31
+# if shutil.which(executable_name):
32
+# PrintStyle(font_color="green", padding=True).print(
33
+# f"'{executable_name}' is now available in PATH after install."
34
+# )
35
+# return True # Successfully installed and found
36
+# else:
37
+# PrintStyle(font_color="orange", padding=True).print(
38
+# f"WARNING: npm install of '{package_name}' reported success, but '{executable_name}' still not found in PATH. " +
39
+# "The 'npx' command in settings.json might still be necessary or there might be an issue with PATH."
40
+# )
41
+# return False # Install reported success, but executable not found
42
+# else:
43
+# PrintStyle(font_color="red", padding=True).print(
44
+# f"Failed to install '{package_name}' globally via npm. Return code: {process.returncode}"
45
+# )
46
+# PrintStyle(font_color="red", padding=False).print(f"npm stdout: {process.stdout.strip()}")
47
+# PrintStyle(font_color="red", padding=False).print(f"npm stderr: {process.stderr.strip()}")
48
+# return False # Install failed
49
+# except FileNotFoundError:
50
+# PrintStyle(font_color="red", padding=True).print(
51
+# f"ERROR: 'npm' command not found. Cannot attempt to install '{package_name}'."
52
+# )
53
+# return False # npm not found
54
+# except Exception as e:
55
+# PrintStyle(font_color="red", padding=True).print(
56
+# f"Exception during npm install of '{package_name}': {e}"
57
+# )
58
+# return False # Other exception during install
59
+# else:
60
+# PrintStyle(font_color="green", padding=True).print(
61
+# f"'{executable_name}' (from package '{package_name}') already found in PATH."
62
+# )
63
+# return True # Already found in PATH
64
+# else:
65
+# PrintStyle(font_color="red", padding=True).print(
66
+# f"ERROR: 'npm' command not found. Cannot check for or install '{executable_name}' from '{package_name}'."
67
+# )
68
+# return False # npm command not found, cannot check or install
69
+# # Fallback, though logic above should cover all paths to return explicitly
70
+# return False
71
72
73
def initialize():
74
- global _NPM_CHECKS_DONE
74
+ # global _NPM_CHECKS_DONE
75
current_settings = settings.get_settings()
76
- mcp_servers_json_string = current_settings.get("mcp_servers", "[]")
76
+ # mcp_servers_json_string = current_settings.get("mcp_servers", "[]")
77
78
- try:
79
- mcp_server_configs = json.loads(mcp_servers_json_string)
80
- if not isinstance(mcp_server_configs, list):
81
- PrintStyle(font_color="red", padding=True).print(
82
- f"Error: Parsed mcp_servers from settings is not a list. Value: {mcp_server_configs}"
83
- )
84
- mcp_server_configs = []
85
- except json.JSONDecodeError as e:
86
- PrintStyle(font_color="red", padding=True).print(
87
- f"Error decoding mcp_servers JSON string from settings: {e}. String was: '{mcp_servers_json_string}'"
88
- )
89
- mcp_server_configs = []
78
+ # try:
79
+ # mcp_server_configs = json.loads(mcp_servers_json_string)
80
+ # if not isinstance(mcp_server_configs, list):
81
+ # PrintStyle(font_color="red", padding=True).print(
82
+ # f"Error: Parsed mcp_servers from settings is not a list. Value: {mcp_server_configs}"
83
+ # )
84
+ # mcp_server_configs = []
85
+ # except json.JSONDecodeError as e:
86
+ # PrintStyle(font_color="red", padding=True).print(
87
+ # f"Error decoding mcp_servers JSON string from settings: {e}. String was: '{mcp_servers_json_string}'"
88
+ # )
89
+ # mcp_server_configs = []
90
91
- if not _NPM_CHECKS_DONE:
92
- if shutil.which("npm"):
93
- for server_config in mcp_server_configs:
94
- if not isinstance(server_config, dict):
95
- PrintStyle(font_color="orange", padding=True).print(
96
- f"Warning: Skipping MCP server config item as it's not a dictionary: {server_config}"
97
- )
98
- continue
91
+ # if not _NPM_CHECKS_DONE:
92
+ # if shutil.which("npm"):
93
+ # for server_config in mcp_server_configs:
94
+ # if not isinstance(server_config, dict):
95
+ # PrintStyle(font_color="orange", padding=True).print(
96
+ # f"Warning: Skipping MCP server config item as it's not a dictionary: {server_config}"
97
+ # )
98
+ # continue
99
100
- command = server_config.get("command")
101
- args = server_config.get("args", [])
102
- server_name = server_config.get("name", "Unknown MCP Server")
100
+ # command = server_config.get("command")
101
+ # args = server_config.get("args", [])
102
+ # server_name = server_config.get("name", "Unknown MCP Server")
103
104
- if command == "npx":
105
- package_name_for_install = None
106
- executable_name_to_check = None
104
+ # if command == "npx":
105
+ # package_name_for_install = None
106
+ # executable_name_to_check = None
107
108
- if "--package" in args: # Original logic for npx --package <pkg> <exec>
109
- try:
110
- package_keyword_index = args.index("--package")
111
- # Expect package name at +1 and executable name at +2 from "--package"
112
- if package_keyword_index + 2 < len(args):
113
- package_name_for_install = args[package_keyword_index + 1]
114
- executable_name_to_check = args[package_keyword_index + 2]
115
- else:
116
- PrintStyle(font_color="orange", padding=True).print(
117
- f"Warning: Skipping MCP server '{server_name}' (npx --package) as package or executable name could not be determined from args: {args}"
118
- )
119
- except ValueError: # Should not happen if "--package" is in args, but good for safety
120
- PrintStyle(font_color="orange", padding=True).print(
121
- f"Warning: '--package' keyword found but .index() failed for args: {args} in server '{server_name}'"
122
- )
123
- except Exception as e:
124
- PrintStyle(font_color="red", padding=True).print(
125
- f"Error processing npx --package args for server '{server_name}': {e}. Args: {args}"
126
- )
127
- else: # New logic for npx <pkg_arg> syntax
128
- parsed_npx_pkg_arg = None
129
- arg_idx = 0
130
- while arg_idx < len(args):
131
- current_arg = args[arg_idx]
132
- # npx's own -p/--package option for temporary installs, distinct from the --package marker we check above
133
- if current_arg == "-p" or current_arg == "--package":
134
- arg_idx += 1 # Move to the value of -p/--package
135
- if arg_idx < len(args): # Ensure there is a value
136
- arg_idx += 1 # Skip the value itself
137
- continue
108
+ # if "--package" in args: # Original logic for npx --package <pkg> <exec>
109
+ # try:
110
+ # package_keyword_index = args.index("--package")
111
+ # # Expect package name at +1 and executable name at +2 from "--package"
112
+ # if package_keyword_index + 2 < len(args):
113
+ # package_name_for_install = args[package_keyword_index + 1]
114
+ # executable_name_to_check = args[package_keyword_index + 2]
115
+ # else:
116
+ # PrintStyle(font_color="orange", padding=True).print(
117
+ # f"Warning: Skipping MCP server '{server_name}' (npx --package) as package or executable name could not be determined from args: {args}"
118
+ # )
119
+ # except ValueError: # Should not happen if "--package" is in args, but good for safety
120
+ # PrintStyle(font_color="orange", padding=True).print(
121
+ # f"Warning: '--package' keyword found but .index() failed for args: {args} in server '{server_name}'"
122
+ # )
123
+ # except Exception as e:
124
+ # PrintStyle(font_color="red", padding=True).print(
125
+ # f"Error processing npx --package args for server '{server_name}': {e}. Args: {args}"
126
+ # )
127
+ # else: # New logic for npx <pkg_arg> syntax
128
+ # parsed_npx_pkg_arg = None
129
+ # arg_idx = 0
130
+ # while arg_idx < len(args):
131
+ # current_arg = args[arg_idx]
132
+ # # npx's own -p/--package option for temporary installs, distinct from the --package marker we check above
133
+ # if current_arg == "-p" or current_arg == "--package":
134
+ # arg_idx += 1 # Move to the value of -p/--package
135
+ # if arg_idx < len(args): # Ensure there is a value
136
+ # arg_idx += 1 # Skip the value itself
137
+ # continue
138
139
- if current_arg.startswith("-"): # Skip other options like -y, --yes, --no-install etc.
140
- arg_idx += 1
141
- continue
139
+ # if current_arg.startswith("-"): # Skip other options like -y, --yes, --no-install etc.
140
+ # arg_idx += 1
141
+ # continue
142
143
- # Found what we assume is the main package argument for npx
144
- parsed_npx_pkg_arg = current_arg
145
- break # Found the package, stop parsing args for this purpose
143
+ # # Found what we assume is the main package argument for npx
144
+ # parsed_npx_pkg_arg = current_arg
145
+ # break # Found the package, stop parsing args for this purpose
146
147
- if parsed_npx_pkg_arg:
148
- package_name_for_install = parsed_npx_pkg_arg
149
- # Derive assumed executable name based on convention from error: "mcp-server-google-maps" for "@.../server-google-maps"
150
- # For "@scope/pkg-name" -> "pkg-name". For "pkg-name" -> "pkg-name".
151
- name_part = parsed_npx_pkg_arg.split("/")[-1]
152
- executable_name_to_check = f"mcp-{name_part}"
153
- PrintStyle(font_color="blue", padding=True).print(
154
- f"Info: For MCP server '{server_name}' (npx <pkg_arg> type), attempting to ensure global install of package '{package_name_for_install}' and expecting executable '{executable_name_to_check}'."
155
- )
156
- else:
157
- PrintStyle(font_color="orange", padding=True).print(
158
- f"Warning: Skipping MCP server '{server_name}' (npx <pkg_arg> type) as main package argument could not be identified from args: {args}"
159
- )
147
+ # if parsed_npx_pkg_arg:
148
+ # package_name_for_install = parsed_npx_pkg_arg
149
+ # # Derive assumed executable name based on convention from error: "mcp-server-google-maps" for "@.../server-google-maps"
150
+ # # For "@scope/pkg-name" -> "pkg-name". For "pkg-name" -> "pkg-name".
151
+ # name_part = parsed_npx_pkg_arg.split("/")[-1]
152
+ # executable_name_to_check = f"mcp-{name_part}"
153
+ # PrintStyle(font_color="blue", padding=True).print(
154
+ # f"Info: For MCP server '{server_name}' (npx <pkg_arg> type), attempting to ensure global install of package '{package_name_for_install}' and expecting executable '{executable_name_to_check}'."
155
+ # )
156
+ # else:
157
+ # PrintStyle(font_color="orange", padding=True).print(
158
+ # f"Warning: Skipping MCP server '{server_name}' (npx <pkg_arg> type) as main package argument could not be identified from args: {args}"
159
+ # )
160
161
- # Unified call to ensure package is installed
162
- if package_name_for_install and executable_name_to_check:
163
- # Ensure they are not empty strings or just whitespace
164
- if package_name_for_install.strip() and executable_name_to_check.strip():
165
- install_successful = _ensure_mcp_package_globally_installed(package_name_for_install.strip(), executable_name_to_check.strip())
166
- if not install_successful:
167
- PrintStyle(font_color="red", padding=True).print(
168
- f"Disabling MCP server '{server_name}' due to failed setup/validation for package '{package_name_for_install}' and/or executable '{executable_name_to_check}'."
169
- )
170
- server_config["disabled"] = True # Disable this server config
171
- else:
172
- PrintStyle(font_color="orange", padding=True).print(
173
- f"Warning: Skipping MCP server '{server_name}' due to empty package or executable name derived: pkg='{package_name_for_install}', exec='{executable_name_to_check}' from args: {args}. Not attempting install."
174
- )
175
- # Optionally, consider if these should also be marked as disabled,
176
- # though current logic implies they weren't valid enough to attempt install.
177
- # server_config["disabled"] = True
178
- # If package_name_for_install or executable_name_to_check are None or empty,
179
- # it means previous logic decided not to proceed or couldn't determine them,
180
- # and appropriate warnings would have been printed.
181
- else:
182
- PrintStyle(font_color="red", padding=True).print(
183
- "ERROR: 'npm' command not found. Cannot attempt to install any MCP server packages."
184
- )
185
- PrintStyle().print() # Extra blank line after all attempts or npm not found message
186
- _NPM_CHECKS_DONE = True
161
+ # # Unified call to ensure package is installed
162
+ # if package_name_for_install and executable_name_to_check:
163
+ # # Ensure they are not empty strings or just whitespace
164
+ # if package_name_for_install.strip() and executable_name_to_check.strip():
165
+ # install_successful = _ensure_mcp_package_globally_installed(package_name_for_install.strip(), executable_name_to_check.strip())
166
+ # if not install_successful:
167
+ # PrintStyle(font_color="red", padding=True).print(
168
+ # f"Disabling MCP server '{server_name}' due to failed setup/validation for package '{package_name_for_install}' and/or executable '{executable_name_to_check}'."
169
+ # )
170
+ # server_config["disabled"] = True # Disable this server config
171
+ # else:
172
+ # PrintStyle(font_color="orange", padding=True).print(
173
+ # f"Warning: Skipping MCP server '{server_name}' due to empty package or executable name derived: pkg='{package_name_for_install}', exec='{executable_name_to_check}' from args: {args}. Not attempting install."
174
+ # )
175
+ # # Optionally, consider if these should also be marked as disabled,
176
+ # # though current logic implies they weren't valid enough to attempt install.
177
+ # # server_config["disabled"] = True
178
+ # # If package_name_for_install or executable_name_to_check are None or empty,
179
+ # # it means previous logic decided not to proceed or couldn't determine them,
180
+ # # and appropriate warnings would have been printed.
181
+ # else:
182
+ # PrintStyle(font_color="red", padding=True).print(
183
+ # "ERROR: 'npm' command not found. Cannot attempt to install any MCP server packages."
184
+ # )
185
+ # PrintStyle().print() # Extra blank line after all attempts or npm not found message
186
+ # _NPM_CHECKS_DONE = True
187
188
# chat model from user settings
189
chat_llm = ModelConfig(
python/helpers/mcp_handler.py
+14
-13
@@ -417,19 +417,20 @@ class MCPConfig(BaseModel):
417
)
418
419
tool_args = ""
420
- properties: dict[str, Any] = tool["input_schema"]["properties"]
421
- for key, value in properties.items():
422
- tool_args += f" \"{key}\": \"...\",\n"
423
- examples = ""
424
- description = ""
425
- if "examples" in value:
426
- examples = f"(examples: {value['examples']})"
427
- if "description" in value:
428
- description = f": {value['description']}"
429
- prompt += (
430
- f" * {key} ({value['type']}){description} {examples}\n"
431
- )
432
- prompt += "\n"
420
+ if "input_schema" in tool and "properties" in tool["input_schema"]:
421
+ properties: dict[str, Any] = tool["input_schema"]["properties"]
422
+ for key, value in properties.items():
423
+ tool_args += f" \"{key}\": \"...\",\n"
424
+ examples = ""
425
+ description = ""
426
+ if "examples" in value:
427
+ examples = f"(examples: {value['examples']})"
428
+ if "description" in value:
429
+ description = f": {value['description']}"
430
+ prompt += (
431
+ f" * {key} ({value['type']}){description} {examples}\n"
432
+ )
433
+ prompt += "\n"
434
435
prompt += (
436
f"#### Usage:\n"
python/helpers/settings.py
+23
-11
@@ -537,16 +537,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
537
}
538
)
539
540
- agent_fields.append(
541
- {
542
- "id": "mcp_servers",
543
- "title": "MCP Servers",
544
- "description": "(JSON list of) >> RemoteServer <<: [name, url, headers, timeout (opt), sse_read_timeout (opt), disabled (opt)] / >> Local Server <<: [name, command, args, env, encoding (opt), encoding_error_handler (opt), disabled (opt)]",
545
- "type": "textarea",
546
- "value": settings["mcp_servers"],
547
- }
548
- )
549
-
540
agent_section: SettingsSection = {
541
"id": "agent",
542
"title": "Agent Config",
@@ -693,7 +683,28 @@ def convert_out(settings: Settings) -> SettingsOutput:
683
"tab": "agent",
684
}
685
686
+
687
# MCP section
688
+ mcp_client_fields: list[SettingsField] = []
689
+
690
+ mcp_client_fields.append(
691
+ {
692
+ "id": "mcp_servers",
693
+ "title": "MCP Servers",
694
+ "description": "(JSON list of) >> RemoteServer <<: [name, url, headers, timeout (opt), sse_read_timeout (opt), disabled (opt)] / >> Local Server <<: [name, command, args, env, encoding (opt), encoding_error_handler (opt), disabled (opt)]",
695
+ "type": "textarea",
696
+ "value": settings["mcp_servers"],
697
+ }
698
+ )
699
+
700
+ mcp_client_section: SettingsSection = {
701
+ "id": "mcp_client",
702
+ "title": "External MCP Servers",
703
+ "description": "Agent Zero can use external MCP servers, local or remote as tools.",
704
+ "fields": mcp_client_fields,
705
+ "tab": "mcp",
706
+ }
707
+
708
mcp_server_fields: list[SettingsField] = []
709
710
mcp_server_fields.append(
@@ -727,6 +738,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
738
stt_section,
739
api_keys_section,
740
auth_section,
741
+ mcp_client_section,
742
mcp_server_section,
743
dev_section,
744
]
@@ -868,7 +880,6 @@ def get_default_settings() -> Settings:
880
agent_prompts_subdir="default",
881
agent_memory_subdir="default",
882
agent_knowledge_subdir="custom",
871
- mcp_servers="",
883
rfc_auto_docker=True,
884
rfc_url="localhost",
885
rfc_password="",
@@ -879,6 +890,7 @@ def get_default_settings() -> Settings:
890
stt_silence_threshold=0.3,
891
stt_silence_duration=1000,
892
stt_waiting_timeout=2000,
893
+ mcp_servers="",
894
mcp_server_enabled=False,
895
)
896