{"id":675750,"date":"2026-05-17T00:52:13","date_gmt":"2026-05-17T00:52:13","guid":{"rendered":"https:\/\/www.newsbeep.com\/au\/675750\/"},"modified":"2026-05-17T00:52:13","modified_gmt":"2026-05-17T00:52:13","slug":"how-to-build-an-mcp-style-routed-ai-agent-system-with-dynamic-tool-exposure-planning-execution-and-context-injection","status":"publish","type":"post","link":"https:\/\/www.newsbeep.com\/au\/675750\/","title":{"rendered":"How to Build an MCP Style Routed AI Agent System with Dynamic Tool Exposure Planning, Execution, and Context Injection"},"content":{"rendered":"<p>class RoutedAgent:<br \/>\n   def __init__(self, server: MCPToolServer, router: HybridMCPRouter, model: str):<br \/>\n       self.server = server<br \/>\n       self.router = router<br \/>\n       self.model = model<\/p>\n<p>   def discover_exposed_tools(self, exposed_tool_names: List[str]) -&gt; List[Dict[str, Any]]:<br \/>\n       return [t for t in self.server.tools_list() if t[&#8220;name&#8221;] in exposed_tool_names]<\/p>\n<p>   def plan(self, task: str, exposed_tools: List[Dict[str, Any]]) -&gt; PlanOutput:<br \/>\n       instructions = &#8220;&#8221;&#8221;<br \/>\nYou are a planning agent in an MCP-like architecture.<br \/>\nYou can only use the exposed tools.<br \/>\nDecide whether tools are needed.<br \/>\nReturn strict JSON only with keys:<br \/>\nrequires_tools: boolean<br \/>\ntool_calls: array of objects with tool_name and arguments<br \/>\ndirect_answer_allowed: boolean<br \/>\nplanner_note: string<\/p>\n<p>Rules:<br \/>\n&#8211; Use at most 3 tool calls.<br \/>\n&#8211; Only call tools from the exposed list.<br \/>\n&#8211; Arguments must match each tool&#8217;s input schema conceptually.<br \/>\n&#8211; Prefer calling vector_retrieve for conceptual local knowledge.<br \/>\n&#8211; Prefer calling web_search for recent or external information.<br \/>\n&#8211; Prefer dataset_loader if the user asks about a named built-in dataset.<br \/>\n&#8211; Prefer python_exec only when computation or code execution is genuinely useful.<br \/>\n&#8211; Do not fabricate unavailable tools.<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p>       prompt = f&#8221;&#8221;&#8221;<br \/>\nUSER TASK:<br \/>\n{task}<\/p>\n<p>EXPOSED TOOLS:<br \/>\n{json.dumps(exposed_tools, indent=2)}<\/p>\n<p>Return JSON only.<br \/>\n&#8220;&#8221;&#8221;<br \/>\n       obj = llm_json(instructions, prompt)<\/p>\n<p>       raw_tool_calls = obj.get(&#8220;tool_calls&#8221;, [])<br \/>\n       parsed_calls = []<br \/>\n       allowed = {t[&#8220;name&#8221;] for t in exposed_tools}<\/p>\n<p>       for call in raw_tool_calls[:MAX_TOOL_CALLS]:<br \/>\n           name = call.get(&#8220;tool_name&#8221;, &#8220;&#8221;)<br \/>\n           args = call.get(&#8220;arguments&#8221;, {})<br \/>\n           if name in allowed and isinstance(args, dict):<br \/>\n               parsed_calls.append(ToolCall(tool_name=name, arguments=args))<\/p>\n<p>       return PlanOutput(<br \/>\n           requires_tools=bool(obj.get(&#8220;requires_tools&#8221;, False) or parsed_calls),<br \/>\n           tool_calls=parsed_calls,<br \/>\n           direct_answer_allowed=bool(obj.get(&#8220;direct_answer_allowed&#8221;, False)),<br \/>\n           planner_note=obj.get(&#8220;planner_note&#8221;, &#8220;&#8221;),<br \/>\n       )<\/p>\n<p>   def run_tools(self, tool_calls: List[ToolCall]) -&gt; List[ToolResult]:<br \/>\n       results = []<br \/>\n       for tc in tool_calls:<br \/>\n           result = self.server.tools_call(tc.tool_name, tc.arguments)<br \/>\n           results.append(result)<br \/>\n       return results<\/p>\n<p>   def answer(self, task: str, route: RouteDecision, exposed_tools: List[Dict[str, Any]], plan: PlanOutput, results: List[ToolResult]) -&gt; str:<br \/>\n       instructions = &#8220;&#8221;&#8221;<br \/>\nYou are the final answering agent in an MCP-style routed tool system.<br \/>\nUse the routed tools and returned tool outputs to answer the user.<br \/>\nBe concrete, concise, and technically correct.<br \/>\nIf tool outputs are partial, say so.<br \/>\nDo not mention hidden tools that were not exposed.<br \/>\n&#8220;&#8221;&#8221;<\/p>\n<p>       tool_result_payload = [r.model_dump() for r in results]<\/p>\n<p>       prompt = f&#8221;&#8221;&#8221;<br \/>\nUSER TASK:<br \/>\n{task}<\/p>\n<p>ROUTE DECISION:<br \/>\n{route.model_dump_json(indent=2)}<\/p>\n<p>EXPOSED TOOLS:<br \/>\n{json.dumps(exposed_tools, indent=2)}<\/p>\n<p>PLAN:<br \/>\n{plan.model_dump_json(indent=2)}<\/p>\n<p>TOOL RESULTS:<br \/>\n{json.dumps(tool_result_payload, indent=2)}<\/p>\n<p>Now answer the user clearly.<br \/>\n&#8220;&#8221;&#8221;<br \/>\n       resp = client.responses.create(<br \/>\n           model=self.model,<br \/>\n           input=prompt,<br \/>\n           instructions=instructions,<br \/>\n           temperature=0.2<br \/>\n       )<br \/>\n       return resp.output_text<\/p>\n<p>   def run(self, task: str, verbose: bool = True) -&gt; Dict[str, Any]:<br \/>\n       route = self.router.route(task)<br \/>\n       exposed_tools = self.discover_exposed_tools(route.selected_tools)<br \/>\n       plan = self.plan(task, exposed_tools)<br \/>\n       results = self.run_tools(plan.tool_calls) if plan.requires_tools else []<br \/>\n       final_answer = self.answer(task, route, exposed_tools, plan, results)<\/p>\n<p>       payload = {<br \/>\n           &#8220;task&#8221;: task,<br \/>\n           &#8220;route_decision&#8221;: route.model_dump(),<br \/>\n           &#8220;exposed_tools&#8221;: exposed_tools,<br \/>\n           &#8220;plan&#8221;: plan.model_dump(),<br \/>\n           &#8220;tool_results&#8221;: [r.model_dump() for r in results],<br \/>\n           &#8220;final_answer&#8221;: final_answer,<br \/>\n       }<\/p>\n<p>       if verbose:<br \/>\n           console.print(Panel.fit(f&#8221;USER TASK\\n{task}&#8221;, title=&#8221;Input&#8221;))<br \/>\n           pretty_tools_table(exposed_tools, &#8220;Tools Exposed By MCP Router&#8221;)<br \/>\n           console.print(Panel(route.rationale or &#8220;No rationale provided&#8221;, title=&#8221;Router Rationale&#8221;))<br \/>\n           if route.policy_notes:<br \/>\n               console.print(Panel(&#8220;\\n&#8221;.join(f&#8221;- {x}&#8221; for x in route.policy_notes), title=&#8221;Policy Notes&#8221;))<br \/>\n           console.print(Panel(plan.planner_note or &#8220;No planner note provided&#8221;, title=&#8221;Planner Note&#8221;))<\/p>\n<p>           if results:<br \/>\n               for r in results:<br \/>\n                   console.print(Panel.fit(RichJSON.from_data(r.model_dump()), title=f&#8221;Tool Result: {r.tool_name}&#8221;))<br \/>\n           console.print(Panel(final_answer, title=&#8221;Final Answer&#8221;))<\/p>\n<p>       return payload<\/p>\n<p>def mcp_jsonrpc_tools_list(server: MCPToolServer) -&gt; Dict[str, Any]:<br \/>\n   return {<br \/>\n       &#8220;jsonrpc&#8221;: &#8220;2.0&#8221;,<br \/>\n       &#8220;id&#8221;: 1,<br \/>\n       &#8220;result&#8221;: {<br \/>\n           &#8220;tools&#8221;: server.tools_list()<br \/>\n       }<br \/>\n   }<\/p>\n<p>def mcp_jsonrpc_tools_call(server: MCPToolServer, tool_name: str, arguments: Dict[str, Any]) -&gt; Dict[str, Any]:<br \/>\n   result = server.tools_call(tool_name, arguments)<br \/>\n   return {<br \/>\n       &#8220;jsonrpc&#8221;: &#8220;2.0&#8221;,<br \/>\n       &#8220;id&#8221;: 2,<br \/>\n       &#8220;result&#8221;: result.model_dump()<br \/>\n   }<\/p>\n<p>router = HybridMCPRouter(server=server, model=MODEL)<br \/>\nagent = RoutedAgent(server=server, router=router, model=MODEL)<\/p>\n<p>console.print(Panel.fit(&#8220;MCP-STYLE TOOL DISCOVERY&#8221;, title=&#8221;Step 1&#8243;))<br \/>\nconsole.print(RichJSON.from_data(mcp_jsonrpc_tools_list(server)))<\/p>\n<p>demo_tasks = [<br \/>\n   &#8220;Explain how an MCP tool router should expose tools for an agent task about dynamic capability exposure.&#8221;,<br \/>\n   &#8220;Search the web for recent examples of MCP-related developments and summarize them.&#8221;,<br \/>\n   &#8220;Load the iris dataset, inspect its columns and basic stats, and tell me what kind of ML problem it is.&#8221;,<br \/>\n   &#8220;Retrieve local knowledge about context injection and router policies, then explain why restricting tool access helps agent performance.&#8221;,<br \/>\n   &#8220;Use Python to compute the average of [3, 5, 9, 10, 13] and then explain whether python execution was truly necessary.&#8221;,<br \/>\n]<\/p>\n<p>all_runs = []<br \/>\nfor idx, task in enumerate(demo_tasks, start=1):<br \/>\n   console.print(Panel.fit(f&#8221;DEMO RUN {idx}&#8221;, title=&#8221;=&#8221; * 10))<br \/>\n   out = agent.run(task, verbose=True)<br \/>\n   all_runs.append(out)<\/p>\n<p>custom_task = &#8220;Design a routed MCP workflow for an AI research assistant that should use retrieval for local protocol knowledge and web search only when the task explicitly asks for recent information.&#8221;<br \/>\ncustom_run = agent.run(custom_task, verbose=True)<\/p>\n<p>print(&#8220;\\nPROGRAMMATIC EXAMPLE: tools\/list&#8221;)<br \/>\nprint(json.dumps(mcp_jsonrpc_tools_list(server), indent=2))<\/p>\n<p>print(&#8220;\\nPROGRAMMATIC EXAMPLE: tools\/call for vector_retrieve&#8221;)<br \/>\nprint(json.dumps(mcp_jsonrpc_tools_call(server, &#8220;vector_retrieve&#8221;, {&#8220;query&#8221;: &#8220;dynamic capability exposure in MCP routers&#8221;, &#8220;top_k&#8221;: 2}), indent=2))<\/p>\n<p>print(&#8220;\\nPROGRAMMATIC EXAMPLE: tools\/call for dataset_loader&#8221;)<br \/>\nprint(json.dumps(mcp_jsonrpc_tools_call(server, &#8220;dataset_loader&#8221;, {&#8220;name&#8221;: &#8220;iris&#8221;, &#8220;n_rows&#8221;: 5}), indent=2))<\/p>\n<p>print(&#8220;\\nPROGRAMMATIC EXAMPLE: custom final answer&#8221;)<br \/>\nprint(custom_run[&#8220;final_answer&#8221;])<\/p>\n","protected":false},"excerpt":{"rendered":"class RoutedAgent: def __init__(self, server: MCPToolServer, router: HybridMCPRouter, model: str): self.server = server self.router = router self.model =&hellip;\n","protected":false},"author":2,"featured_media":675751,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[20],"tags":[256,254,255,64,63,105],"class_list":["post-675750","post","type-post","status-publish","format-standard","has-post-thumbnail","category-artificial-intelligence","tag-ai","tag-artificial-intelligence","tag-artificialintelligence","tag-au","tag-australia","tag-technology"],"_links":{"self":[{"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/posts\/675750","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/comments?post=675750"}],"version-history":[{"count":0,"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/posts\/675750\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/media\/675751"}],"wp:attachment":[{"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/media?parent=675750"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/categories?post=675750"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.newsbeep.com\/au\/wp-json\/wp\/v2\/tags?post=675750"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}