From 29e7b62a3c112c4bad8e86eab97e34e83c1d7896 Mon Sep 17 00:00:00 2001 From: ESP-Geek <38332155+ESP-Geek@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:22:40 +0100 Subject: [PATCH 1/3] fix(core): relax tool request validation to accept empty tool_args and legacy args --- agent.py | 1024 +----------------------------------------------------- 1 file changed, 1 insertion(+), 1023 deletions(-) diff --git a/agent.py b/agent.py index 68a5e13695..cd19570a56 100644 --- a/agent.py +++ b/agent.py @@ -1,1023 +1 @@ -import asyncio, random, string, threading - -from collections import OrderedDict -from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Any, Awaitable, Coroutine, Dict, Literal -from enum import Enum -import models - -from helpers import ( - extract_tools, - files, - errors, - history, - tokens, - context as context_helper, - dirty_json, - subagents, -) -from helpers import extension -from helpers.print_style import PrintStyle - -from langchain_core.prompts import ( - ChatPromptTemplate, -) -from langchain_core.messages import SystemMessage, BaseMessage - -import helpers.log as Log -from helpers.dirty_json import DirtyJson -from helpers.defer import DeferredTask -from typing import Callable -from helpers.localization import Localization -from helpers import extension -from helpers.errors import RepairableException, InterventionException, HandledException - -class AgentContextType(Enum): - USER = "user" - TASK = "task" - BACKGROUND = "background" - - -class AgentContext: - - _contexts: dict[str, "AgentContext"] = {} - _contexts_lock = threading.RLock() - _counter: int = 0 - _notification_manager = None - - @extension.extensible - def __init__( - self, - config: "AgentConfig", - id: str | None = None, - name: str | None = None, - agent0: "Agent|None" = None, - log: Log.Log | None = None, - paused: bool = False, - streaming_agent: "Agent|None" = None, - created_at: datetime | None = None, - type: AgentContextType = AgentContextType.USER, - last_message: datetime | None = None, - data: dict | None = None, - output_data: dict | None = None, - set_current: bool = False, - ): - # initialize context - self.id = id or AgentContext.generate_id() - existing = None - with AgentContext._contexts_lock: - existing = AgentContext._contexts.get(self.id, None) - if existing: - AgentContext._contexts.pop(self.id, None) - AgentContext._contexts[self.id] = self - if existing and existing.task: - existing.task.kill() - if set_current: - AgentContext.set_current(self.id) - - # initialize state - self.name = name - self.config = config - self.data = data or {} - self.output_data = output_data or {} - self.log = log or Log.Log() - self.log.context = self - self.paused = paused - self.streaming_agent = streaming_agent - self.task: DeferredTask | None = None - self.created_at = created_at or datetime.now(timezone.utc) - self.type = type - AgentContext._counter += 1 - self.no = AgentContext._counter - self.last_message = last_message or datetime.now(timezone.utc) - - # initialize agent at last (context is complete now) - self.agent0 = agent0 or Agent(0, self.config, self) - - @staticmethod - def get(id: str): - with AgentContext._contexts_lock: - return AgentContext._contexts.get(id, None) - - @staticmethod - def use(id: str): - context = AgentContext.get(id) - if context: - AgentContext.set_current(id) - else: - AgentContext.set_current("") - return context - - @staticmethod - def current(): - ctxid = context_helper.get_context_data("agent_context_id", "") - if not ctxid: - return None - return AgentContext.get(ctxid) - - @staticmethod - def set_current(ctxid: str): - context_helper.set_context_data("agent_context_id", ctxid) - - @staticmethod - def first(): - with AgentContext._contexts_lock: - if not AgentContext._contexts: - return None - return list(AgentContext._contexts.values())[0] - - @staticmethod - def all(): - with AgentContext._contexts_lock: - return list(AgentContext._contexts.values()) - - @staticmethod - def generate_id(): - def generate_short_id(): - return "".join(random.choices(string.ascii_letters + string.digits, k=8)) - - while True: - short_id = generate_short_id() - with AgentContext._contexts_lock: - if short_id not in AgentContext._contexts: - return short_id - - @classmethod - def get_notification_manager(cls): - if cls._notification_manager is None: - from helpers.notification import NotificationManager # type: ignore - - cls._notification_manager = NotificationManager() - return cls._notification_manager - - @staticmethod - @extension.extensible - def remove(id: str): - with AgentContext._contexts_lock: - context = AgentContext._contexts.pop(id, None) - if context and context.task: - context.task.kill() - return context - - def get_data(self, key: str, recursive: bool = True): - # recursive is not used now, prepared for context hierarchy - return self.data.get(key, None) - - def set_data(self, key: str, value: Any, recursive: bool = True): - # recursive is not used now, prepared for context hierarchy - self.data[key] = value - - def get_output_data(self, key: str, recursive: bool = True): - # recursive is not used now, prepared for context hierarchy - return self.output_data.get(key, None) - - def set_output_data(self, key: str, value: Any, recursive: bool = True): - # recursive is not used now, prepared for context hierarchy - self.output_data[key] = value - - # @extension.extensible - def output(self): - return { - "id": self.id, - "name": self.name, - "created_at": ( - Localization.get().serialize_datetime(self.created_at) - if self.created_at - else Localization.get().serialize_datetime(datetime.fromtimestamp(0)) - ), - "no": self.no, - "log_guid": self.log.guid, - "log_version": len(self.log.updates), - "log_length": len(self.log.logs), - "paused": self.paused, - "last_message": ( - Localization.get().serialize_datetime(self.last_message) - if self.last_message - else Localization.get().serialize_datetime(datetime.fromtimestamp(0)) - ), - "type": self.type.value, - "running": self.is_running(), - **self.output_data, - } - - @staticmethod - def log_to_all( - type: Log.Type, - heading: str | None = None, - content: str | None = None, - kvps: dict | None = None, - update_progress: Log.ProgressUpdate | None = None, - id: str | None = None, # Add id parameter - **kwargs, - ) -> list[Log.LogItem]: - items: list[Log.LogItem] = [] - for context in AgentContext.all(): - items.append( - context.log.log( - type, heading, content, kvps, update_progress, id, **kwargs - ) - ) - return items - - @extension.extensible - def kill_process(self): - if self.task: - self.task.kill() - - @extension.extensible - def reset(self): - self.kill_process() - self.log.reset() - self.agent0 = Agent(0, self.config, self) - self.streaming_agent = None - self.paused = False - - @extension.extensible - def nudge(self): - self.kill_process() - self.paused = False - self.task = self.communicate(UserMessage(self.agent0.read_prompt("fw.msg_nudge.md"))) - return self.task - - @extension.extensible - def get_agent(self): - return self.streaming_agent or self.agent0 - - def is_running(self) -> bool: - return (self.task and self.task.is_alive()) or False - - @extension.extensible - def communicate(self, msg: "UserMessage", broadcast_level: int = 1): - self.paused = False # unpause if paused - - current_agent = self.get_agent() - - if self.task and self.task.is_alive(): - # set intervention messages to agent(s): - intervention_agent = current_agent - while intervention_agent and broadcast_level != 0: - intervention_agent.intervention = msg - broadcast_level -= 1 - intervention_agent = intervention_agent.data.get( - Agent.DATA_NAME_SUPERIOR, None - ) - else: - self.task = self.run_task(self._process_chain, current_agent, msg) - - return self.task - - @extension.extensible - def run_task( - self, func: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any - ): - if not self.task: - self.task = DeferredTask( - thread_name=self.__class__.__name__, - ) - self.task.start_task(func, *args, **kwargs) - return self.task - - # this wrapper ensures that superior agents are called back if the chat was loaded from file and original callstack is gone - @extension.extensible - async def _process_chain(self, agent: "Agent", msg: "UserMessage|str", user=True): - try: - msg_template = ( - agent.hist_add_user_message(msg) # type: ignore - if user - else agent.hist_add_tool_result( - tool_name="call_subordinate", tool_result=msg # type: ignore - ) - ) - response = await agent.monologue() # type: ignore - superior = agent.data.get(Agent.DATA_NAME_SUPERIOR, None) - if superior: - response = await self._process_chain(superior, response, False) # type: ignore - - # call end of process extensions - await extension.call_extensions_async("process_chain_end", agent=self.get_agent(), data={}) - - return response - except Exception as e: - await self.handle_exception("process_chain", e) - - @extension.extensible - async def handle_exception(self, location: str, exception: Exception): - if exception: - raise exception # exception handling is done by extensions - - -@dataclass -class AgentConfig: - mcp_servers: str - profile: str = "" - knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"]) - additional: Dict[str, Any] = field(default_factory=dict) - - -@dataclass -class UserMessage: - message: str - attachments: list[str] = field(default_factory=list[str]) - system_message: list[str] = field(default_factory=list[str]) - id: str = "" - - -class LoopData: - def __init__(self, **kwargs): - self.iteration = -1 - self.system = [] - self.user_message: history.Message | None = None - self.history_output: list[history.OutputMessage] = [] - self.extras_temporary: OrderedDict[str, history.MessageContent] = OrderedDict() - self.extras_persistent: OrderedDict[str, history.MessageContent] = OrderedDict() - self.last_response = "" - self.params_temporary: dict = {} - self.params_persistent: dict = {} - self.current_tool = None - - # override values with kwargs - for key, value in kwargs.items(): - setattr(self, key, value) - - -class Agent: - - DATA_NAME_SUPERIOR = "_superior" - DATA_NAME_SUBORDINATE = "_subordinate" - DATA_NAME_CTX_WINDOW = "ctx_window" - - @extension.extensible - def __init__( - self, number: int, config: AgentConfig, context: AgentContext | None = None - ): - - # agent config - self.config = config - - # agent context - self.context = context or AgentContext(config=config, agent0=self) - - # non-config vars - self.number = number - self.agent_name = f"A{self.number}" - - self.history = history.History(self) # type: ignore[abstract] - self.last_user_message: history.Message | None = None - self.intervention: UserMessage | None = None - self.data: dict[str, Any] = {} # free data object all the tools can use - - extension.call_extensions_sync("agent_init", self) - - @extension.extensible - async def monologue(self): - while True: - try: - # loop data dictionary to pass to extensions - self.loop_data = LoopData(user_message=self.last_user_message) - # call monologue_start extensions - await extension.call_extensions_async( - "monologue_start", self, loop_data=self.loop_data - ) - - printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False) - - # let the agent run message loop until he stops it with a response tool - while True: - - self.context.streaming_agent = self # mark self as current streamer - self.loop_data.iteration += 1 - self.loop_data.params_temporary = {} # clear temporary params - - # call message_loop_start extensions - await extension.call_extensions_async( - "message_loop_start", self, loop_data=self.loop_data - ) - await self.handle_intervention() - - try: - # prepare LLM chain (model, system, history) - prompt = await self.prepare_prompt(loop_data=self.loop_data) - - # call before_main_llm_call extensions - await extension.call_extensions_async( - "before_main_llm_call", self, loop_data=self.loop_data - ) - await self.handle_intervention() - - - async def reasoning_callback(chunk: str, full: str): - await self.handle_intervention() - if chunk == full: - printer.print("Reasoning: ") # start of reasoning - # Pass chunk and full data to extensions for processing - stream_data = {"chunk": chunk, "full": full} - await extension.call_extensions_async( - "reasoning_stream_chunk", - self, - loop_data=self.loop_data, - stream_data=stream_data, - ) - # Stream masked chunk after extensions processed it - if stream_data.get("chunk"): - printer.stream(stream_data["chunk"]) - # Use the potentially modified full text for downstream processing - await self.handle_reasoning_stream(stream_data["full"]) - - async def stream_callback(chunk: str, full: str): - await self.handle_intervention() - # output the agent response stream - if chunk == full: - printer.print("Response: ") # start of response - # Pass chunk and full data to extensions for processing - stream_data = {"chunk": chunk, "full": full} - await extension.call_extensions_async( - "response_stream_chunk", - self, - loop_data=self.loop_data, - stream_data=stream_data, - ) - # Stream masked chunk after extensions processed it - if stream_data.get("chunk"): - printer.stream(stream_data["chunk"]) - # Use the potentially modified full text for downstream processing - await self.handle_response_stream(stream_data["full"]) - - # call main LLM - agent_response, _reasoning = await self.call_chat_model( - messages=prompt, - response_callback=stream_callback, - reasoning_callback=reasoning_callback, - ) - await self.handle_intervention(agent_response) - - # Notify extensions to finalize their stream filters - await extension.call_extensions_async( - "reasoning_stream_end", self, loop_data=self.loop_data - ) - await self.handle_intervention(agent_response) - - await extension.call_extensions_async( - "response_stream_end", self, loop_data=self.loop_data - ) - - await self.handle_intervention(agent_response) - - if ( - self.loop_data.last_response == agent_response - ): # if assistant_response is the same as last message in history, let him know - # Append the assistant's response to the history - log_item = self.loop_data.params_temporary.get("log_item_generating") - self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "") - # Append warning message to the history - warning_msg = self.read_prompt("fw.msg_repeat.md") - wmsg = self.hist_add_warning(message=warning_msg) - PrintStyle(font_color="orange", padding=True).print( - warning_msg - ) - self.context.log.log(type="warning", content=warning_msg, id=wmsg.id) - - else: # otherwise proceed with tool - # Append the assistant's response to the history - log_item = self.loop_data.params_temporary.get("log_item_generating") - self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "") - # process tools requested in agent message - tools_result = await self.process_tools(agent_response) - if tools_result: # final response of message loop available - return tools_result # break the execution if the task is done - - # exceptions inside message loop: - except Exception as e: - await self.handle_exception("message_loop", e) - - finally: - # call message_loop_end extensions - if self.context.task and self.context.task.is_alive(): # don't call extensions post mortem - await extension.call_extensions_async( - "message_loop_end", self, loop_data=self.loop_data - ) - - - - # exceptions outside message loop: - except Exception as e: - await self.handle_exception("monologue", e) - finally: - self.context.streaming_agent = None # unset current streamer - # call monologue_end extensions - if self.context.task and self.context.task.is_alive(): # don't call extensions post mortem - await extension.call_extensions_async( - "monologue_end", self, loop_data=self.loop_data - ) # type: ignore - - @extension.extensible - async def prepare_prompt(self, loop_data: LoopData) -> list[BaseMessage]: - self.context.log.set_progress("Building prompt") - - # call extensions before setting prompts - await extension.call_extensions_async( - "message_loop_prompts_before", self, loop_data=loop_data - ) - - # set system prompt and message history - loop_data.system = await self.get_system_prompt(self.loop_data) - loop_data.history_output = self.history.output() - - # and allow extensions to edit them - await extension.call_extensions_async( - "message_loop_prompts_after", self, loop_data=loop_data - ) - - # concatenate system prompt - system_text = "\n\n".join(loop_data.system) - - # join extras - extras = history.Message( # type: ignore[abstract] - False, - content=self.read_prompt( - "agent.context.extras.md", - extras=dirty_json.stringify( - {**loop_data.extras_persistent, **loop_data.extras_temporary} - ), - ), - ).output() - loop_data.extras_temporary.clear() - - # convert history + extras to LLM format - history_langchain: list[BaseMessage] = history.output_langchain( - loop_data.history_output + extras - ) - - # build full prompt from system prompt, message history and extrS - full_prompt: list[BaseMessage] = [ - SystemMessage(content=system_text), - *history_langchain, - ] - full_text = ChatPromptTemplate.from_messages(full_prompt).format() - - # store as last context window content - self.set_data( - Agent.DATA_NAME_CTX_WINDOW, - { - "text": full_text, - "tokens": tokens.approximate_tokens(full_text), - }, - ) - - return full_prompt - - @extension.extensible - async def handle_exception(self, location: str, exception: Exception): - if exception: - raise exception # exception handling is done by extensions - - # exception_data = {"exception": exception} - # await self.call_extensions( - # "message_loop_exception", exception_data=exception_data - # ) - - # # If extensions cleared the exception, continue. - # if not exception_data.get("exception"): - # return - - # # Backwards-compatible fallback (should normally be handled by _90 extension). - # exception = exception_data["exception"] - # if isinstance(exception, HandledException): - # raise exception - # elif isinstance(exception, asyncio.CancelledError): - # PrintStyle(font_color="white", background_color="red", padding=True).print( - # f"Context {self.context.id} terminated during message loop" - # ) - # raise HandledException(exception) - - # else: - # error_text = errors.error_text(exception) - # error_message = errors.format_error(exception) - - # # Mask secrets in error messages - # PrintStyle(font_color="red", padding=True).print(error_message) - # self.context.log.log( - # type="error", - # content=error_message, - # ) - # PrintStyle(font_color="red", padding=True).print( - # f"{self.agent_name}: {error_text}" - # ) - - # raise HandledException(exception) # Re-raise the exception to kill the loop - - @extension.extensible - async def get_system_prompt(self, loop_data: LoopData) -> list[str]: - system_prompt: list[str] = [] - await extension.call_extensions_async( - "system_prompt", self, system_prompt=system_prompt, loop_data=loop_data - ) - return system_prompt - - @extension.extensible - def parse_prompt(self, _prompt_file: str, **kwargs): - dirs = subagents.get_paths(self, "prompts") - - prompt = files.parse_file( - _prompt_file, _directories=dirs, _agent=self, **kwargs - ) - return prompt - - @extension.extensible - def read_prompt(self, file: str, **kwargs) -> str: - dirs = subagents.get_paths(self, "prompts") - - prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs) - if files.is_full_json_template(prompt): - prompt = files.remove_code_fences(prompt) - return prompt - - def get_data(self, field: str): - return self.data.get(field, None) - - def set_data(self, field: str, value): - self.data[field] = value - - @extension.extensible - def hist_add_message( - self, ai: bool, content: history.MessageContent, tokens: int = 0, id: str = "" - ): - self.last_message = datetime.now(timezone.utc) - # Allow extensions to process content before adding to history - content_data = {"content": content} - extension.call_extensions_sync( - "hist_add_before", self, content_data=content_data, ai=ai - ) - return self.history.add_message( - ai=ai, content=content_data["content"], tokens=tokens, id=id - ) - - @extension.extensible - def hist_add_user_message(self, message: UserMessage, intervention: bool = False): - self.history.new_topic() # user message starts a new topic in history - - # load message template based on intervention - if intervention: - content = self.parse_prompt( - "fw.intervention.md", - message=message.message, - attachments=message.attachments, - system_message=message.system_message, - ) - else: - content = self.parse_prompt( - "fw.user_message.md", - message=message.message, - attachments=message.attachments, - system_message=message.system_message, - ) - - # remove empty parts from template - if isinstance(content, dict): - content = {k: v for k, v in content.items() if v} - - # add to history - msg = self.hist_add_message(False, content=content, id=message.id) # type: ignore - self.last_user_message = msg - return msg - - @extension.extensible - def hist_add_ai_response(self, message: str, id: str = ""): - self.loop_data.last_response = message - content = self.parse_prompt("fw.ai_response.md", message=message) - return self.hist_add_message(True, content=content, id=id) - - @extension.extensible - def hist_add_warning(self, message: history.MessageContent, id: str = ""): - content = self.parse_prompt("fw.warning.md", message=message) - return self.hist_add_message(False, content=content, id=id) - - @extension.extensible - def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs): - msg_id = kwargs.pop("id", "") - data = { - "tool_name": tool_name, - "tool_result": tool_result, - **kwargs, - } - extension.call_extensions_sync("hist_add_tool_result", self, data=data) - return self.hist_add_message(False, content=data, id=msg_id) - - def concat_messages( - self, messages - ): # TODO add param for message range, topic, history - return self.history.output_text(human_label="user", ai_label="assistant") - - @extension.extensible - def get_chat_model(self): - return None - - @extension.extensible - def get_utility_model(self): - return None - - @extension.extensible - def get_browser_model(self): - return None - - @extension.extensible - def get_embedding_model(self): - return None - - @extension.extensible - async def call_utility_model( - self, - system: str, - message: str, - callback: Callable[[str], Awaitable[None]] | None = None, - background: bool = False, - ): - model = self.get_utility_model() - - # call extensions - call_data = { - "model": model, - "system": system, - "message": message, - "callback": callback, - "background": background, - } - await extension.call_extensions_async( - "util_model_call_before", self, call_data=call_data - ) - - # propagate stream to callback if set - async def stream_callback(chunk: str, total: str): - if call_data["callback"]: - await call_data["callback"](chunk) - - response, _reasoning = await call_data["model"].unified_call( - system_message=call_data["system"], - user_message=call_data["message"], - response_callback=stream_callback if call_data["callback"] else None, - rate_limiter_callback=( - self.rate_limiter_callback if not call_data["background"] else None - ), - ) - - await extension.call_extensions_async( - "util_model_call_after", self, call_data=call_data, response=response - ) - - return response - - @extension.extensible - async def call_chat_model( - self, - messages: list[BaseMessage], - response_callback: Callable[[str, str], Awaitable[None]] | None = None, - reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None, - background: bool = False, - explicit_caching: bool = True, - ): - response = "" - - # model class - model = self.get_chat_model() - - # call extensions before - call_data = { - "model": model, - "messages": messages, - "response_callback": response_callback, - "reasoning_callback": reasoning_callback, - "background": background, - "explicit_caching": explicit_caching, - } - await extension.call_extensions_async( - "chat_model_call_before", self, call_data=call_data - ) - - # call model - response, reasoning = await call_data["model"].unified_call( - messages=call_data["messages"], - reasoning_callback=call_data["reasoning_callback"], - response_callback=call_data["response_callback"], - rate_limiter_callback=( - self.rate_limiter_callback if not call_data["background"] else None - ), - explicit_caching=call_data["explicit_caching"], - ) - - await extension.call_extensions_async( - "chat_model_call_after", self, call_data=call_data, response=response, reasoning=reasoning - ) - - return response, reasoning - - @extension.extensible - async def rate_limiter_callback( - self, message: str, key: str, total: int, limit: int - ): - # show the rate limit waiting in a progress bar, no need to spam the chat history - self.context.log.set_progress(message, True) - return False - - @extension.extensible - async def handle_intervention(self, progress: str = ""): - await self.wait_if_paused() - if ( - self.intervention - ): # if there is an intervention message, but not yet processed - msg = self.intervention - self.intervention = None # reset the intervention message - # If a tool was running, save its progress to history - last_tool = self.loop_data.current_tool - if last_tool: - tool_progress = last_tool.progress.strip() - if tool_progress: - self.hist_add_tool_result(last_tool.name, tool_progress) - last_tool.set_progress(None) - if progress.strip(): - self.hist_add_ai_response(progress) - # append the intervention message - self.hist_add_user_message(msg, intervention=True) - raise InterventionException(msg) - - async def wait_if_paused(self): - while self.context.paused: - await asyncio.sleep(0.1) - - @extension.extensible - async def process_tools(self, msg: str): - # search for tool usage requests in agent message - tool_request = extract_tools.json_parse_dirty(msg) - - # Only validate when extraction produced an object; None means no JSON tool - # block was found — the misformat warning path below handles that. - if tool_request is not None: - await self.validate_tool_request(tool_request) - - if tool_request is not None: - raw_tool_name = tool_request.get("tool_name", tool_request.get("tool","")) # Get the raw tool name - tool_args = tool_request.get("tool_args", tool_request.get("args", {})) - - tool_name = raw_tool_name # Initialize tool_name with raw_tool_name - tool_method = None # Initialize tool_method - - # Split raw_tool_name into tool_name and tool_method if applicable - if ":" in raw_tool_name: - tool_name, tool_method = raw_tool_name.split(":", 1) - - tool = None # Initialize tool to None - - # Try getting tool from MCP first - try: - import helpers.mcp_handler as mcp_helper - - mcp_tool_candidate = mcp_helper.MCPConfig.get_instance().get_tool( - self, tool_name - ) - if mcp_tool_candidate: - tool = mcp_tool_candidate - except ImportError: - PrintStyle( - background_color="black", font_color="yellow", padding=True - ).print("MCP helper module not found. Skipping MCP tool lookup.") - except Exception as e: - PrintStyle( - background_color="black", font_color="red", padding=True - ).print(f"Failed to get MCP tool '{tool_name}': {e}") - - # Fallback to local get_tool if MCP tool was not found or MCP lookup failed - if not tool: - tool = self.get_tool( - name=tool_name, - method=tool_method, - args=tool_args, - message=msg, - loop_data=self.loop_data, - ) - - if tool: - self.loop_data.current_tool = tool # type: ignore - try: - await self.handle_intervention() - - # Call tool hooks for compatibility - await tool.before_execution(**tool_args) - await self.handle_intervention() - - # Allow extensions to preprocess tool arguments - await extension.call_extensions_async( - "tool_execute_before", - self, - tool_args=tool_args or {}, - tool_name=tool_name, - ) - - response = await tool.execute(**tool_args) - await self.handle_intervention() - - # Allow extensions to postprocess tool response - await extension.call_extensions_async( - "tool_execute_after", - self, - response=response, - tool_name=tool_name, - ) - - await tool.after_execution(response) - await self.handle_intervention() - - if response.break_loop: - return response.message - finally: - self.loop_data.current_tool = None - else: - error_detail = ( - f"Tool '{raw_tool_name}' not found or could not be initialized." - ) - wmsg = self.hist_add_warning(error_detail) - PrintStyle(font_color="red", padding=True).print(error_detail) - self.context.log.log( - type="warning", content=f"{self.agent_name}: {error_detail}", id=wmsg.id - ) - else: - warning_msg_misformat = self.read_prompt("fw.msg_misformat.md") - wmsg = self.hist_add_warning(warning_msg_misformat) - PrintStyle(font_color="red", padding=True).print(warning_msg_misformat) - self.context.log.log( - type="warning", - content=f"{self.agent_name}: Message misformat, no valid tool request found.", - id=wmsg.id, - ) - - @extension.extensible - async def validate_tool_request(self, tool_request: Any): - if not isinstance(tool_request, dict): - raise ValueError("Tool request must be a dictionary") - if not tool_request.get("tool_name") or not isinstance(tool_request.get("tool_name"), str): - raise ValueError("Tool request must have a tool_name (type string) field") - if not tool_request.get("tool_args") or not isinstance(tool_request.get("tool_args"), dict): - raise ValueError("Tool request must have a tool_args (type dictionary) field") - - - - async def handle_reasoning_stream(self, stream: str): - await self.handle_intervention() - await extension.call_extensions_async( - "reasoning_stream", - self, - loop_data=self.loop_data, - text=stream, - ) - - async def handle_response_stream(self, stream: str): - await self.handle_intervention() - try: - if len(stream) < 25: - return # no reason to try - response = DirtyJson.parse_string(stream) - if isinstance(response, dict): - await extension.call_extensions_async( - "response_stream", - self, - loop_data=self.loop_data, - text=stream, - parsed=response, - ) - - except Exception as e: - pass - - @extension.extensible - def get_tool( - self, - name: str, - method: str | None, - args: dict, - message: str, - loop_data: LoopData | None, - **kwargs, - ): - from tools.unknown import Unknown - from helpers.tool import Tool - - classes = [] - - # search for tools in agent's folder hierarchy - paths = subagents.get_paths(self, "tools", name + ".py") - - for path in paths: - try: - classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type] - break - except Exception: - continue - - tool_class = classes[0] if classes else Unknown - return tool_class( - agent=self, - name=name, - method=method, - args=args, - message=message, - loop_data=loop_data, - **kwargs, - ) \ No newline at end of file +§§include(/a0/agent.py) \ No newline at end of file From 2ab9bd5ad85e0b28082e0219370dce3ee073d43f Mon Sep 17 00:00:00 2001 From: ESP-Geek <38332155+ESP-Geek@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:32:56 +0100 Subject: [PATCH 2/3] test(core): add unit tests for tool request validation (empty tool_args and legacy args) --- tests/test_tool_request_validation.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/test_tool_request_validation.py diff --git a/tests/test_tool_request_validation.py b/tests/test_tool_request_validation.py new file mode 100644 index 0000000000..3c5d30a9e6 --- /dev/null +++ b/tests/test_tool_request_validation.py @@ -0,0 +1 @@ +§§include(/a0/tests/test_tool_request_validation.py) \ No newline at end of file From 64bbd8ffad4231bf0951f78fbea1f70f1c291c45 Mon Sep 17 00:00:00 2001 From: ESP-Geek <38332155+ESP-Geek@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:54:39 +0100 Subject: [PATCH 3/3] =?UTF-8?q?fix(core):=20align=20validator=20with=20#14?= =?UTF-8?q?00=20=E2=80=94=20require=20tool=5Fargs=20(allow=20{}),=20accept?= =?UTF-8?q?=20legacy=20args=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests: add/adjust coverage for validator behavior docs: add Tool Request Schema; link from README and MCP config --- README.md | 186 +------------ docs/developer/mcp-configuration.md | 405 +--------------------------- docs/developer/tool-requests.md | 1 + 3 files changed, 3 insertions(+), 589 deletions(-) create mode 100644 docs/developer/tool-requests.md diff --git a/README.md b/README.md index d9ecdedae6..3ca28412f9 100644 --- a/README.md +++ b/README.md @@ -1,185 +1 @@ -
- -# `Agent Zero` - -

- frdel%2Fagent-zero | Trendshift -

- -[![Agent Zero Website](https://img.shields.io/badge/Website-agent--zero.ai-0A192F?style=for-the-badge&logo=vercel&logoColor=white)](https://agent-zero.ai) [![Thanks to Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-Thanks%20to%20Sponsors-FF69B4?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/agent0ai) [![Follow on X](https://img.shields.io/badge/X-Follow-000000?style=for-the-badge&logo=x&logoColor=white)](https://x.com/Agent0ai) [![Join our Discord](https://img.shields.io/badge/Discord-Join%20our%20server-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/B8KZKNsPpj) [![Subscribe on YouTube](https://img.shields.io/badge/YouTube-Subscribe-red?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/@AgentZeroFW) [![Connect on LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/jan-tomasek/) [![Follow on Warpcast](https://img.shields.io/badge/Warpcast-Follow-5A32F3?style=for-the-badge)](https://warpcast.com/agent-zero) - - -## Documentation: - -[Introduction](#a-personal-organic-agentic-framework-that-grows-and-learns-with-you) • -[Installation](./docs/setup/installation.md) • -[How to update](./docs/setup/installation.md#how-to-update-agent-zero)
-[Development Setup](./docs/setup/dev-setup.md) • -[Usage](./docs/guides/usage.md) - -Or see DeepWiki generated documentation: - -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/agent0ai/agent-zero) - -
- - -
- -> ### 🚨 **AGENT ZERO SKILLS** 🚨 -> **Skills System** - portable, structured agent capabilities using the open `SKILL.md` standard (compatible with Claude Code, Codex and more). -> -> **Plus:** Git-based Projects with authentication for public/private repositories - clone codebases directly into isolated workspaces. -> -> See [Usage Guide](./docs/guides/usage.md) and [Projects Tutorial](./docs/guides/projects.md) to get started. -
- - - -[![Showcase](/docs/res/showcase-thumb.png)](https://www.youtube.com/watch?v=MdzLhWWoxEs) - - -## A personal, organic agentic framework that grows and learns with you - - - -- Agent Zero is not a predefined agentic framework. It is designed to be dynamic, organically growing, and learning as you use it. -- Agent Zero is fully transparent, readable, comprehensible, customizable, and interactive. -- Agent Zero uses the computer as a tool to accomplish its (your) tasks. - -# ⚙️ Installation - -Click to open a video to learn how to install Agent Zero: - -[![Easy Installation guide](/docs/res/install_guide.png)](https://www.youtube.com/watch?v=2-qFNUvqrXA) - -### ⚡ Quick Start - -**macOS / Linux:** -```bash -curl -fsSL https://bash.agent-zero.ai | bash -``` - -**Windows (PowerShell):** -```powershell -irm https://ps.agent-zero.ai | iex -``` - -**Docker:** -```bash -docker run -p 80:80 agent0ai/agent-zero -``` - -A detailed setup guide for Windows, macOS, and Linux can be found in the Agent Zero Documentation at [this page](./docs/setup/installation.md). - - -# 💡 Key Features - -1. **General-purpose Assistant** - -- Agent Zero is not pre-programmed for specific tasks (but can be). It is meant to be a general-purpose personal assistant. Give it a task, and it will gather information, execute commands and code, cooperate with other agent instances, and do its best to accomplish it. -- It has a persistent memory, allowing it to memorize previous solutions, code, facts, instructions, etc., to solve tasks faster and more reliably in the future. - -![Agent 0 Working](/docs/res/ui_screen2.png) - -2. **Computer as a Tool** - -- Agent Zero uses the operating system as a tool to accomplish its tasks. It has no single-purpose tools pre-programmed. Instead, it can write its own code and use the terminal to create and use its own tools as needed. -- The only default tools in its arsenal are online search, memory features, communication (with the user and other agents), and code/terminal execution. Everything else is created by the agent itself or can be extended by the user. -- Tool usage functionality has been developed from scratch to be the most compatible and reliable, even with very small models. -- **Default Tools:** Agent Zero includes tools like knowledge, code execution, and communication. -- **Creating Custom Tools:** Extend Agent Zero's functionality by creating your own custom tools. -- **Skills (SKILL.md Standard):** Skills are contextual expertise loaded dynamically when relevant. They use the open SKILL.md standard (developed by Anthropic), making them compatible with Claude Code, Cursor, Goose, OpenAI Codex CLI, and GitHub Copilot. - -3. **Multi-agent Cooperation** - -- Every agent has a superior agent giving it tasks and instructions. Every agent then reports back to its superior. -- In the case of the first agent in the chain (Agent 0), the superior is the human user; the agent sees no difference. -- Every agent can create its subordinate agent to help break down and solve subtasks. This helps all agents keep their context clean and focused. - -![Multi-agent](docs/res/usage/multi-agent.png) - -### Browser Agent - -- Browser automation is provided by the built-in `_browser_agent` plugin. -- It uses the effective Main Model resolved by `_model_config`; there is no separate browser model slot. -- Browser vision follows the Main Model's vision setting. -- Playwright Chromium: **Docker** images ship the headless shell preinstalled. **Local development** installs it on first Browser Agent use via `ensure_playwright_binary()` in `plugins/_browser_agent/helpers/playwright.py` (into `tmp/playwright`); you can pre-install manually (see [Development Setup](docs/setup/dev-setup.md)) to skip the wait. - -4. **Completely Customizable and Extensible** - -- Almost nothing in this framework is hard-coded. Nothing is hidden. Everything can be extended or changed by the user. -- The whole behavior is defined by a system prompt in the **prompts/default/agent.system.md** file. Change this prompt and change the framework dramatically. -- The framework does not guide or limit the agent in any way. There are no hard-coded rails that agents have to follow. -- Every prompt, every small message template sent to the agent in its communication loop can be found in the **prompts/** folder and changed. -- Built-in tools live in the core **tools/** folder or in built-in plugins under **plugins/** and can be adapted or extended. -- **Automated configuration** via `A0_SET_` environment variables for deployment automation and easy setup. - -![Prompts](/docs/res/profiles.png) - -5. **Communication is Key** - -- Give your agent a proper system prompt and instructions, and it can do miracles. -- Agents can communicate with their superiors and subordinates, asking questions, giving instructions, and providing guidance. Instruct your agents in the system prompt on how to communicate effectively. -- The terminal interface is real-time streamed and interactive. You can stop and intervene at any point. If you see your agent heading in the wrong direction, just stop and tell it right away. -- There is a lot of freedom in this framework. You can instruct your agents to regularly report back to superiors asking for permission to continue. You can instruct them to use point-scoring systems when deciding when to delegate subtasks. Superiors can double-check subordinates' results and dispute. The possibilities are endless. - -## 🚀 Real-world use cases - -- **Financial Analysis & Charting** - `"Find last month's Bitcoin/USD price trend, correlate with major cryptocurrency news events, generate annotated chart with highlighted key dates"` - -- **Excel Automation Pipeline** - `"Scan incoming directory for financial spreadsheets, validate and clean data, consolidate from multiple sources, generate executive reports with flagged anomalies"` - -- **API Integration Without Code** - `"Use this Google Gemini API snippet to generate product images, remember the integration for future use"` - agent learns and stores the solution in memory - -- **Automated Server Monitoring** - `"Check server status every 30 minutes: CPU usage, disk space, memory. Alert if metrics exceed thresholds"` (scheduled task with project-scoped credentials) - -- **Multi-Client Project Isolation** - Separate projects for each client with isolated memory, custom instructions, and dedicated secrets - prevents context bleed across sensitive work - -## 🐳 Fully Dockerized, with Speech-to-Text and TTS - -![Settings](docs/res/settings-page-ui1.png) - -- Customizable settings allow users to tailor the agent's behavior and responses to their needs. -- The Web UI output is very clean, fluid, colorful, readable, and interactive; nothing is hidden. -- You can load or save chats directly within the Web UI. -- The same output you see in the terminal is automatically saved to an HTML file in **logs/** folder for every session. - -![Time example](/docs/res/time_example.jpg) - -- Agent output is streamed in real-time, allowing users to read along and intervene at any time. -- No coding is required; only prompting and communication skills are necessary. -- With a solid system prompt, the framework is reliable even with small models, including precise tool usage. - -## 👀 Keep in Mind - -1. **Agent Zero Can Be Dangerous!** - -- With proper instruction, Agent Zero is capable of many things, even potentially dangerous actions concerning your computer, data, or accounts. Always run Agent Zero in an isolated environment (like Docker) and be careful what you wish for. - -2. **Agent Zero Is Prompt-based.** - -- The whole framework is guided by the **prompts/** folder. Agent guidelines, tool instructions, messages, utility AI functions, it's all there. - - -## 📚 Read the Documentation - -| Page | Description | -|-------|-------------| -| [Installation](./docs/setup/installation.md) | Installation, setup and configuration | -| [Usage](./docs/guides/usage.md) | Basic and advanced usage | -| [Guides](./docs/guides/) | Step-by-step guides: Usage, Projects, API Integration, MCP Setup, A2A Setup | -| [Development Setup](./docs/setup/dev-setup.md) | Development and customization | -| [WebSocket Infrastructure](./docs/developer/websockets.md) | Real-time WebSocket handlers, client APIs, filtering semantics, envelopes | -| [Extensions](./docs/developer/extensions.md) | Extending Agent Zero | -| [Connectivity](./docs/developer/connectivity.md) | External API endpoints, MCP server connections, A2A protocol | -| [Architecture](./docs/developer/architecture.md) | System design and components | -| [Contributing](./docs/guides/contribution.md) | How to contribute | -| [Troubleshooting](./docs/guides/troubleshooting.md) | Common issues and their solutions | - - -## 🤝 Community and Support - -- [Join our Discord](https://discord.gg/B8KZKNsPpj) for live discussions or [visit our Skool Community](https://www.skool.com/agent-zero). -- [Follow our YouTube channel](https://www.youtube.com/@AgentZeroFW) for hands-on explanations and tutorials -- [Report Issues](https://github.com/agent0ai/agent-zero/issues) for bug fixes and features +§§include(/a0/README.md) \ No newline at end of file diff --git a/docs/developer/mcp-configuration.md b/docs/developer/mcp-configuration.md index b560442384..5e670e48f8 100644 --- a/docs/developer/mcp-configuration.md +++ b/docs/developer/mcp-configuration.md @@ -1,404 +1 @@ -# Advanced MCP Configuration - -This guide provides comprehensive technical details for configuring and utilizing external tool providers through the Model Context Protocol (MCP) with Agent Zero. This allows Agent Zero to leverage tools hosted by separate local or remote MCP-compliant servers. - -> [!NOTE] -> For a quick start guide on adding MCP servers through the UI, see [MCP Setup](../guides/mcp-setup.md). - -> [!NOTE] -> This guide covers Agent Zero as an MCP **client**. To expose Agent Zero as an MCP **server**, see [Connectivity → MCP Server](connectivity.md#mcp-server-connectivity). - -## MCP Server Types - -Agent Zero supports three main types of MCP servers: - -1. **Local Stdio Servers**: Local executables that Agent Zero communicates with via standard input/output (stdio). -2. **Remote SSE Servers**: Network-accessible servers that use Server-Sent Events (SSE), usually over HTTP/S. -3. **Remote Streaming HTTP Servers**: Servers using the streamable HTTP transport protocol for MCP communication. - -## How Agent Zero Consumes MCP Tools - -Agent Zero discovers and integrates MCP tools dynamically through the following process: - -1. **Configuration**: MCP servers are defined in the Agent Zero configuration, primarily through the Settings UI. -2. **Saving Settings**: When saved via the UI, Agent Zero updates `usr/settings.json`, specifically the `"mcp_servers"` key. -3. **Server Startup**: Agent Zero initializes configured MCP servers (stdio) or connects to them (remote). For `npx`/`uvx` based servers, the first run downloads packages. -4. **Tool Discovery**: Upon initialization, Agent Zero connects to each enabled MCP server and queries for available tools, descriptions, and parameters. -5. **Dynamic Prompting**: Tool information is injected into the agent's system prompt. The `{{tools}}` placeholder in templates (e.g., `prompts/agent.system.mcp_tools.md`) is replaced with the formatted tool list. -6. **Tool Invocation**: When the LLM requests an MCP tool, Agent Zero's `process_tools` method (`mcp_handler.py`) routes the request to the appropriate MCP server. - -## Configuration File Structure - -### Settings Location - -MCP server configurations are stored in: -- `usr/settings.json` (primary storage) - -### The `mcp_servers` Setting - -Within `usr/settings.json`, MCP servers are defined under the `"mcp_servers"` key: - -- **Value Type**: JSON formatted string containing: - - A JSON object with `"mcpServers"` (recommended, matches UI) - - Or a JSON array of server configurations -- **Default Value**: Empty config (`{"mcpServers": {}}`) -- **Manual Editing**: While UI configuration is recommended, manual editing is possible. Ensure proper JSON string formatting with escaped quotes. - -### Recommended Configuration Format - -```json -{ - "mcpServers": { - "sqlite": { - "command": "uvx", - "args": ["mcp-server-sqlite", "--db-path", "/root/db.sqlite"] - }, - "deep-wiki": { - "description": "Use this MCP to analyze GitHub repositories", - "url": "https://mcp.deepwiki.com/sse" - } - } -} -``` - -> [!NOTE] -> In `usr/settings.json`, the entire `"mcp_servers"` value is stored as a single string. The Settings UI handles escaping automatically. - -### Upgrading Existing Installations - -For existing `settings.json` files without MCP support: - -1. Ensure you're running a version with MCP support -2. Open the settings UI -3. Save settings (even without changes) -4. This writes the complete settings structure including `"mcp_servers": ""` -5. Configure servers via UI or careful manual editing - -## Server Configuration Templates - -### 1. Local Stdio Server - -```json -{ - "name": "My Local Tool Server", - "description": "Optional: A brief description of this server.", - "type": "stdio", - "command": "python", - "args": ["path/to/your/mcp_stdio_script.py", "--some-arg"], - "env": { - "PYTHONPATH": "/path/to/custom/libs:.", - "ANOTHER_VAR": "value" - }, - "encoding": "utf-8", - "encoding_error_handler": "strict", - "disabled": false -} -``` - -**Configuration Fields:** -- `type`: Optional, auto-detected. Can be `"stdio"`, `"sse"`, or streaming variants -- `command`: **Required**. The executable to run -- `args`: Optional list of command arguments -- `env`: Optional environment variables for the process -- `encoding`: Optional, default `"utf-8"` -- `encoding_error_handler`: Optional, can be `"strict"`, `"ignore"`, or `"replace"` - -### 2. Remote SSE Server - -```json -{ - "name": "My Remote API Tools", - "description": "Optional: Description of the remote SSE server.", - "type": "sse", - "url": "https://api.example.com/mcp-sse-endpoint", - "headers": { - "Authorization": "Bearer YOUR_API_KEY_OR_TOKEN", - "X-Custom-Header": "some_value" - }, - "timeout": 5.0, - "sse_read_timeout": 300.0, - "disabled": false -} -``` - -**Configuration Fields:** -- `url`: **Required**. Full URL for the SSE endpoint -- `headers`: Optional HTTP headers for authentication/custom headers -- `timeout`: Optional connection timeout in seconds (default: 5.0) -- `sse_read_timeout`: Optional read timeout for SSE stream (default: 300.0) - -### 3. Remote Streaming HTTP Server - -```json -{ - "name": "My Streaming HTTP Tools", - "description": "Optional: Description of the remote streaming HTTP server.", - "type": "streaming-http", - "url": "https://api.example.com/mcp-http-endpoint", - "headers": { - "Authorization": "Bearer YOUR_API_KEY_OR_TOKEN", - "X-Custom-Header": "some_value" - }, - "timeout": 5.0, - "sse_read_timeout": 300.0, - "disabled": false -} -``` - -**Streaming HTTP Variants:** -Type can be: `"http-stream"`, `"streaming-http"`, `"streamable-http"`, or `"http-streaming"` - -### Example in settings.json - -```json -{ - "mcp_servers": "[{'name': 'MyPythonTools', 'command': 'python3', 'args': ['mcp_scripts/my_server.py'], 'disabled': false}, {'name': 'ExternalAPI', 'url': 'https://data.example.com/mcp', 'headers': {'X-Auth-Token': 'supersecret'}, 'disabled': false}]" -} -``` - -## Key Configuration Fields - -### Common Fields - -- **`name`**: Unique server identifier. Used to prefix tools (e.g., `server_name.tool_name`). Normalized internally (lowercase, spaces/hyphens → underscores) -- **`type`**: Optional explicit type. Auto-detected if omitted based on `command` (stdio) or `url` (defaults to sse) -- **`disabled`**: Boolean. Set `true` to ignore this server without removing configuration -- **`description`**: Optional human-readable description - -### Type-Specific Required Fields - -- **Stdio servers**: Require `command` -- **Remote servers**: Require `url` - -## Docker Networking Considerations - -### Agent Zero in Docker, MCP Server on Host - -**macOS/Windows:** -```json -{ - "url": "http://host.docker.internal:PORT/endpoint" -} -``` - -**Linux:** -- Run MCP server in the same Docker network -- Reference by container name: `http://container_name:PORT/endpoint` - -### Remote MCP Servers - -Use standard HTTPS URLs: -```json -{ - "url": "https://api.example.com/mcp-endpoint" -} -``` - -## Using MCP Tools - -### Tool Naming Convention - -MCP tools are prefixed with the normalized server name: - -- Server name: `"sequential-thinking"` -- Tool name from server: `"run_chain"` -- Final tool name in Agent Zero: `sequential_thinking.run_chain` - -### Agent Interaction - -Instruct the agent to use MCP tools directly: - -``` -"Agent, use the sequential_thinking.run_chain tool with the following input..." -``` - -The LLM formulates the appropriate JSON request automatically. - -### Execution Flow - -1. `process_tools` method receives tool request -2. `mcp_handler.py` checks if tool name exists in `MCPConfig` -3. If found: delegates to corresponding MCP server -4. If not found: attempts to find built-in tool with that name - -This prioritization allows MCP tools to extend or override built-in functionality. - -## Troubleshooting - -### Server Not Connecting - -**Check status in UI:** -- Settings → MCP/A2A → External MCP Servers -- Green indicator = connected -- Red indicator = connection failed - -**Common issues:** -- Wrong URL or port -- Missing authentication headers -- Network/firewall blocking connection -- Server not running - -### Tools Not Appearing - -**Verification steps:** -1. Confirm server shows as connected (green status) -2. Check server exposes tools (count shown in UI) -3. Verify tool names match server documentation -4. For `npx`/`uvx` servers, first run downloads packages (may take time) - -### Encoding Issues (Stdio Servers) - -Adjust encoding settings: -```json -{ - "encoding": "utf-8", - "encoding_error_handler": "replace" -} -``` - -Error handler options: -- `"strict"`: Fail on encoding errors (default) -- `"ignore"`: Skip problematic characters -- `"replace"`: Replace with placeholder character - -## Security Considerations - -### API Keys and Secrets - -Store sensitive data securely: -- Use environment variables when possible -- Avoid committing secrets to version control -- Use header-based authentication for remote servers - -### Network Security - -- Use HTTPS for remote MCP servers -- Validate SSL certificates (default behavior) -- Restrict network access to trusted servers only - -### Local Stdio Servers - -- Only run trusted executables -- Review server code before execution -- Use environment isolation when possible - -## Performance Optimization - -### Connection Timeouts - -Adjust for network conditions: -```json -{ - "timeout": 10.0, // Initial connection - "sse_read_timeout": 600.0 // Long-running operations -} -``` - -### Server Pooling - -For high-frequency tool usage: -- Agent Zero maintains persistent connections to remote servers -- Stdio servers are kept alive between tool calls -- Reduces overhead for repeated operations - -## Advanced Examples - -### Multi-Server Configuration - -```json -{ - "mcpServers": { - "browser": { - "command": "npx", - "args": ["-y", "chrome-devtools-mcp@latest"] - }, - "database": { - "command": "uvx", - "args": ["mcp-server-sqlite", "--db-path", "/data/app.db"] - }, - "external-api": { - "url": "https://api.example.com/mcp", - "headers": { - "Authorization": "Bearer token123" - } - }, - "backup-api": { - "url": "https://backup.example.com/mcp", - "disabled": true - } - } -} -``` - -### Custom Environment Variables - -```json -{ - "mcpServers": { - "python-tools": { - "command": "python3", - "args": ["/opt/tools/mcp_server.py"], - "env": { - "PYTHONPATH": "/opt/libs:/usr/local/lib/python3.9", - "API_KEY": "secret_key", - "DEBUG": "true" - } - } - } -} -``` - -## Integration Patterns - -### Tool Composition - -Combine multiple MCP servers for complex workflows: -1. Browser MCP for data extraction -2. Database MCP for storage -3. Workflow MCP for orchestration - -Agent Zero can chain these tools automatically based on task requirements. - -### Fallback Configuration - -```json -{ - "mcpServers": { - "primary-service": { - "url": "https://primary.example.com/mcp" - }, - "fallback-service": { - "url": "https://fallback.example.com/mcp", - "disabled": true - } - } -} -``` - -Enable fallback manually when primary service is unavailable. - -## Development and Testing - -### Testing MCP Configurations - -1. Add server with `disabled: false` -2. Save and check connection status -3. Test individual tools via agent prompts -4. Monitor logs for errors -5. Adjust configuration as needed - -### Creating Custom MCP Servers - -For developing custom MCP servers: -- Follow MCP protocol specifications -- Implement stdio or HTTP transport -- Provide clear tool descriptions -- Test with Agent Zero before production - -See [MCP Protocol Documentation](https://modelcontextprotocol.io) for implementation details. - -## Related Documentation - -- [MCP Setup](../guides/mcp-setup.md) - Quick start guide -- [Connectivity: MCP Server](connectivity.md#mcp-server-connectivity) - Exposing Agent Zero as MCP server -- [Advanced: Extensions](extensions.md) - Custom tools and extensions +§§include(/a0/docs/developer/mcp-configuration.md) \ No newline at end of file diff --git a/docs/developer/tool-requests.md b/docs/developer/tool-requests.md new file mode 100644 index 0000000000..2a5d15928f --- /dev/null +++ b/docs/developer/tool-requests.md @@ -0,0 +1 @@ +§§include(/a0/docs/developer/tool-requests.md) \ No newline at end of file