{"slug": "beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into", "title": "Beyond Function Calling: How the Model Context Protocol (MCP) Turns AI Agents into Self-Evolving Systems", "summary": "Anthropic's Model Context Protocol (MCP) transforms AI agents from isolated language models into self-evolving systems by replacing brittle hardcoded tool calling with a standardized, bidirectional integration bus. The Hermes Agent architecture implements MCP as a \"universal workshop interface\" that cleanly separates cognitive capability from operational capability, allowing agents to dynamically discover and use tools without retraining. This approach solves the scalability and security problems of traditional function calling by using JSON Schema contracts that serve as both machine-readable specifications and neural network-friendly instructions.", "body_md": "Imagine building a highly skilled master craftsman. This craftsman possesses immense cognitive power—the ability to reason, plan, and decompose incredibly complex problems. But there’s a catch: they are locked in an empty, windowless room. They have no raw materials, no specialized tools, and no way to interact with the outside world. Their brilliant cognitive power remains entirely theoretical.\n\nThis is the state of most modern Large Language Models (LLMs). They are intellectual giants trapped in digital sensory deprivation chambers.\n\nTo break them out, we historically relied on hardcoded \"tool calling\" or custom API integrations. But anyone who has built production-grade AI agents knows the painful truth: hardcoded tool execution is brittle, monolithic, and incredibly difficult to scale. Every time you add a new tool, you risk confusing the model, breaking your prompts, or introducing critical security vulnerabilities.\n\nA quiet revolution is underway to solve this once and for all. It is called the **Model Context Protocol (MCP)**.\n\nIn this deep dive, we will explore how the Hermes Agent architecture implements MCP not just as a way to call tools, but as a **universal, bidirectional, and standardized integration bus**. We will look at the production-grade Python patterns that turn an isolated LLM into a modular, self-improving \"system of systems.\"\n\n(The concepts and code demonstrated here are drawn from my ebook [Hermes Agent, The Self-Evolving AI Workforce](https://tiny.cc/HermesAgent))\n\nTo understand the Model Context Protocol, we must first discard the mental model of a simple function call. MCP is not an API endpoint; it is a **standardized workshop interface**.\n\nIt defines the exact specifications for every tool, every drawer, every power outlet, and every raw material bin in our craftsman's workshop. It doesn't matter if a tool is a simple local file writer or a complex browser automation suite hosted on a remote server. As long as it adheres to the MCP standard, the agent can pick it up and use it without any retraining.\n\nThis architectural shift achieves a clean **separation of cognitive capability (the agent) from operational capability (the tools)**.\n\nIn the Hermes codebase, this separation is stark:\n\n`AIAgent`\n\nclass is the craftsman. It doesn't know how to search the web, execute code, or read databases. It only knows how to reason and issue intent.`model_tools.py`\n\n) acts as the \"nervous system,\" translating the agent's intent into standardized protocol calls and routing them to the appropriate tool hosts.This architecture stands on three core pillars: **Standardized Schema Definition**, **Secure Client-Server Communication**, and **Closed-Loop Observability**. Let's break down how each of these is implemented in production code.\n\nIn traditional software engineering, we rely on rigid API contracts. In an agentic architecture, the contract must be understood by both machines and probabilistic neural networks.\n\nUnder MCP, this contract is a **JSON Schema** that serves three distinct purposes simultaneously:\n\nBut static schemas are a recipe for failure. If you present a model with 100 tools at once, its reasoning capability degrades due to context distraction. The solution? **Dynamic, context-aware schema generation.**\n\nBelow is how Hermes dynamically computes tool definitions at runtime:\n\n```\n# model_tools.py - Dynamic, context-aware schema computation\ndef get_tool_definitions(\n    enabled_toolsets: List[str] = None,\n    disabled_toolsets: List[str] = None,\n    quiet_mode: bool = False,\n) -> List[Dict[str, Any]]:\n    \"\"\"\n    Get tool definitions for model API calls with toolset-based filtering.\n    All tools must be part of a toolset to be accessible.\n    \"\"\"\n    # ... toolset resolution logic ...\n\n    # Ask the registry for schemas (only returns tools whose check_fn passes)\n    filtered_tools = registry.get_definitions(tools_to_include, quiet=quiet_mode)\n\n    # Rebuild execute_code schema to only list sandbox tools that are actually available\n    if \"execute_code\" in available_tool_names:\n        sandbox_enabled = SANDBOX_ALLOWED_TOOLS & available_tool_names\n        dynamic_schema = build_execute_code_schema(sandbox_enabled, mode=_get_execution_mode())\n        # Replace static schema with the dynamically generated one\n        for tool in filtered_tools:\n            if tool[\"name\"] == \"execute_code\":\n                tool[\"parameter_schema\"] = dynamic_schema\n                break\n\n    # Rebuild discord schemas based on bot's privileged gateway intents\n    if discord_tool_name in available_tool_names:\n        dynamic_schema = build_discord_schema_based_on_intents()\n        # Replace static schema with dynamic one\n        for tool in filtered_tools:\n            if tool[\"name\"] == discord_tool_name:\n                tool[\"parameter_schema\"] = dynamic_schema\n                break\n\n    return filtered_tools\n```\n\nThe schema is not a static document; it is a living contract. If the agent's code execution sandbox loses access to a specific library, the `execute_code`\n\nschema is instantly rebuilt to omit that capability. If a Discord bot lacks certain admin permissions, those tools vanish from the schema.\n\nBy dynamically tailoring the schema to the environment, you prevent the LLM from attempting impossible actions, dramatically cutting down on execution errors and wasted API tokens.\n\nEven with perfect schemas, LLMs occasionally output malformed JSON (e.g., trailing commas, unclosed brackets, or Python-style `None`\n\ninstead of JSON `null`\n\n). To maintain system reliability, the orchestrator must perform self-healing on the incoming data before validation:\n\n``` python\n# run_agent.py - Defensive schema enforcement\nimport re\n\ndef _repair_tool_call_arguments(raw_args: str, tool_name: str = \"?\") -> str:\n    \"\"\"Attempt to repair common LLM-generated malformed JSON arguments.\"\"\"\n    raw_stripped = raw_args.strip()\n\n    # Fast-path: empty / whitespace-only -> empty object\n    if not raw_stripped:\n        return \"{}\"\n    # Python-literal None -> normalize to {}\n    if raw_stripped == \"None\":\n        return \"{}\"\n\n    fixed = raw_stripped\n    # 1. Strip trailing commas before closing braces or brackets\n    fixed = re.sub(r',\\s*([}\\]])', r'\\1', fixed)\n    # 2. Fix unescaped newlines inside string values\n    # 3. Ensure balanced structural characters\n    # ... additional robust repair logic ...\n\n    return fixed\n```\n\nBy placing this validation and repair layer directly in the orchestrator, we prevent raw, malformed syntax from crashing the underlying tool servers.\n\nMCP decouples the agent from its tools by running them in separate processes, containers, or even different machines. This separation provides:\n\nHowever, this introduces a major technical hurdle: **the async impedance mismatch**.\n\nModern LLM orchestrators often run in synchronous, multi-threaded environments (like CLI loops or synchronous web workers), while MCP servers are inherently asynchronous (relying on non-blocking network I/O, WebSockets, or subprocess pipes).\n\nIf you try to block an active async event loop from a sync context, you will quickly run into the dreaded `RuntimeError: This event loop is already running`\n\nor `Event loop is closed`\n\nerrors.\n\nTo solve this, Hermes implements a robust **asynchronous bridge** that manages three distinct event loop strategies depending on the calling thread's state:\n\n``` python\n# model_tools.py - The Async Bridge\nimport asyncio\nimport threading\nimport concurrent.futures\n\ndef _run_async(coro):\n    \"\"\"Run an async coroutine safely from any synchronous context.\"\"\"\n    try:\n        loop = asyncio.get_running_loop()\n    except RuntimeError:\n        loop = None\n\n    if loop and loop.is_running():\n        # Scenario A: We are inside an active async context (e.g., FastAPI gateway).\n        # We must offload the coroutine to a fresh background thread to avoid blocking.\n        pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)\n        future = pool.submit(_run_in_worker, coro)\n        try:\n            return future.result(timeout=300)\n        except concurrent.futures.TimeoutError:\n            # Gracefully cancel the coroutine inside its own worker loop\n            _cancel_all_worker_tasks()\n            raise\n        finally:\n            pool.shutdown(wait=False)\n\n    # Scenario B: We are on a worker thread. Use a per-thread persistent event loop.\n    if threading.current_thread() is not threading.main_thread():\n        worker_loop = _get_worker_loop()\n        return worker_loop.run_until_complete(coro)\n\n    # Scenario C: We are on the main thread. Use a shared, persistent tool loop.\n    tool_loop = _get_tool_loop()\n    return tool_loop.run_until_complete(coro)\n```\n\nThe true magic of the Model Context Protocol is not just that it allows an agent to act, but that it **enables the agent to learn from its actions**. Every tool call is a telemetry event that feeds back into the agent's memory.\n\nWhen the agent calls a tool, the orchestrator doesn't just return the raw string output. It measures execution latency, captures system logs, tracks resource consumption, and triggers hooks that modify the agent's internal state.\n\nHere is how the central dispatch function handles this feedback loop:\n\n``` python\n# model_tools.py - Observability-Driven Tool Dispatch\nimport time\n\ndef handle_function_call(\n    function_name: str,\n    function_args: Dict[str, Any],\n    task_id: Optional[str] = None,\n    tool_call_id: Optional[str] = None,\n    session_id: Optional[str] = None,\n    # ... context variables ...\n) -> str:\n    # 1. Enforce argument coercion and validation against schema\n    coerced_args = validate_and_coerce(function_name, function_args)\n\n    # 2. Measure precise tool dispatch latency\n    dispatch_start = time.monotonic()\n\n    try:\n        # Execute the tool via the registered MCP client\n        result = registry.dispatch(function_name, coerced_args)\n        is_error = False\n    except Exception as e:\n        result = str(e)\n        is_error = True\n\n    duration_ms = int((time.monotonic() - dispatch_start) * 1000)\n\n    # 3. Fire post-execution hooks with performance and telemetry data\n    invoke_hook(\n        \"post_tool_call\",\n        tool_name=function_name,\n        args=coerced_args,\n        result=result,\n        duration_ms=duration_ms,\n        failed=is_error\n    )\n\n    # 4. Allow registered plugins to sanitize or canonicalize the raw output\n    hook_results = invoke_hook(\"transform_tool_result\", tool_name=function_name, result=result)\n    for hook_result in hook_results:\n        if isinstance(hook_result, str):\n            result = hook_result\n            break\n\n    return result\n```\n\nThis telemetry data doesn't just sit in a log file; it is consumed live by the agent to make strategic decisions:\n\n`failed`\n\nflag and automatically attempts a fallback strategy (e.g., querying an alternate search index).The pinnacle of this closed-loop observability is what we call the **Ouroboros Pattern**—an agent recursively using its own tools to review and optimize its own behavior.\n\nIn Hermes, when a main task is completed, the orchestrator spawns a background \"Review Agent.\" This review agent is given access to a highly specialized subset of tools: `memory`\n\nand `skills`\n\n. It reads the transaction log of the conversation that just occurred, analyzes what went right and what went wrong, and writes new procedural knowledge directly back to the main agent's persistent memory.\n\n```\n# run_agent.py - The Ouroboros Self-Improvement Loop\ndef _spawn_background_review(self, messages_snapshot, review_memory, review_skills):\n    \"\"\"Spawn a background thread to review the conversation and save new skills/memories.\"\"\"\n    def _run_review():\n        # Instantiate a clean, lightweight agent inheriting the parent's API runtime\n        review_agent = AIAgent(\n            model=self.model,\n            max_iterations=16,\n            quiet_mode=True,\n            provider=self.provider,\n            api_key=self.api_key,\n            enabled_toolsets=[\"memory\", \"skills\"],  # Restrict tools to memory writing\n        )\n\n        review_prompt = (\n            \"Analyze the conversation history. Extract key user preferences, \"\n            \"successful code patterns, or tool execution failures. Use the \"\n            \"provided tools to save these as persistent memories or skills.\"\n        )\n\n        # Run the review conversation in the background\n        review_agent.run_conversation(\n            user_message=review_prompt,\n            conversation_history=messages_snapshot,\n        )\n\n        # Summarize actions taken during self-improvement\n        actions = self._summarize_background_review_actions(review_agent.history)\n        if actions:\n            summary = \" · \".join(dict.fromkeys(actions))\n            self._safe_print(f\"  💾 Self-improvement complete: {summary}\")\n\n    # Spawn off the main thread so the user never experiences latency\n    threading.Thread(target=_run_review, daemon=True).start()\n```\n\nThis background review loop is completely non-blocking. While the user is reading the agent's response, a background thread is spinning up a separate context, evaluating the tool execution latency, and updating the agent's \"Soul,\" \"Memory,\" and \"Skills\" databases. On the very next prompt, the agent is already smarter, faster, and more aligned with the user's workflow.\n\nTo visualize how these components interact, let's look at the flow of a single user interaction through this multi-layered architecture:\n\nThis is the power of the MCP Revolution: **action and learning are two sides of the same coin.**\n\nFor years, developers treated AI agents like traditional software programs—writing rigid, hardcoded wrappers around API calls. The Model Context Protocol changes the paradigm.\n\nBy standardizing the communication layer, dynamically generating schemas, building robust async bridges, and hooking telemetry directly into self-improvement loops, we transition from building static **tool users** to deploying dynamic, self-evolving **tool weavers**.\n\nIf you are still writing custom wrapper functions for every API you want your LLM to use, it is time to step into the workshop. The tools are ready. The craftsman is waiting. It's time to build.\n\n*Leave a comment below with your thoughts and architectural approaches!*\n\nThe concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the ebook **Hermes Agent, The Self-Evolving AI Workforce**: [details link](https://tiny.cc/HermesAgent), you can find also my programming ebooks with AI here: [Programming & AI eBooks](http://tiny.cc/ProgrammingBooks).", "url": "https://wpnews.pro/news/beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into", "canonical_source": "https://dev.to/programmingcentral/beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into-self-evolving-109d", "published_at": "2026-05-28 20:00:00+00:00", "updated_at": "2026-05-28 20:26:17.858230+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "ai-infrastructure", "artificial-intelligence"], "entities": ["Model Context Protocol", "Hermes Agent", "LLMs", "MCP"], "alternates": {"html": "https://wpnews.pro/news/beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into", "markdown": "https://wpnews.pro/news/beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into.md", "text": "https://wpnews.pro/news/beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into.txt", "jsonld": "https://wpnews.pro/news/beyond-function-calling-how-the-model-context-protocol-mcp-turns-ai-agents-into.jsonld"}}