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