Have you ever felt like your AI agent is a brilliant intern who’s also incredibly distractible? You give it a simple task, but because it has access to a dozen different tools—a web browser, a code interpreter, a database—it gets confused, picks the wrong one, or just hallucinates a tool that doesn’t even exist.
It’s a common problem. We want our agents to be capable, so we load them up with every tool we can think of. But this often backfires. It's like putting a chef in a kitchen with every gadget ever invented and asking them to make a simple omelet. They might spend more time trying to figure out whether to use the sous-vide machine than actually cracking the eggs.
What if there was a better way? What if, instead of giving the agent the entire kitchen, we had a smart manager—a sous-chef—who looked at the recipe and said, "For this omelet, you'll need a whisk, a pan, and a spatula. That's it."
That’s exactly what we’re going to build today. We're going to create an AI agent system with a "router" that intelligently decides which tools to show the agent for any given task. This concept, inspired by principles from the Model Context Protocol (MCP), is all about dynamic capability exposure. It’s a fancy term for a simple, powerful idea: only give the agent the tools it actually needs, right when it needs them.
This makes the agent faster, less prone to error, and a whole lot easier to understand. So, grab a coffee, and let's build a smarter, more focused AI assistant from the ground up.
First Things First: Setting Up Our Workshop
Before we start building our agent, we need to get our workspace organized. This is the part where we lay out all our parts and make sure we have the right nuts and bolts. In our case, that means setting up the Python environment and defining the "language" our system will use to communicate.
We'll be using a handful of common Python libraries like OpenAI for the brain, Pydantic for creating structured data models, and a few others for things like web search and data handling. If you've worked with AI agents before, this should look pretty familiar.
The really important part here is defining our data structures. Think of these as blueprints. We need a clear, consistent way to describe a tool, a plan, a decision, and a result. Without this, different parts of our system would be speaking different languages, leading to chaos.
So, we create simple classes for things like:
ToolSpec: The "ID card" for each tool. It lists its name, what it does, and what kind of input it needs.ToolCall: A specific request to use a tool, like "use theweb_searchtool with the query 'latest AI news'."RouteDecision: The notes from our "manager" explaining which tools were selected and why.PlanOutput: The agent's step-by-step plan for how it's going to use the tools.ToolResult: The outcome of a tool call, whether it was a success or an error.
By defining these upfront, we ensure that every component, from the router to the agent, knows exactly what to expect. It’s the boring-but-essential groundwork that makes everything else possible.
We’ll also create a small, local knowledge base—just a few text snippets about the concepts we're using. This will let us build a simple retrieval tool, giving our agent a "local memory" it can consult for internal knowledge.
Stocking the Toolshed: Our Agent's Capabilities
Okay, our workshop is clean and our blueprints are ready. Now it's time to build the actual tools our agent can use. An agent is only as good as the tools it has access to, so we'll give it a few powerful, distinct capabilities.
For this project, we're building four core tools:
- Web Search (
tool_web_search): This is the agent's window to the world. When it needs fresh, up-to-the-minute information, it can use this tool to search the internet. Simple, but essential. - Python Executor (
tool_python_exec): This is the agent's calculator and data analysis powerhouse. It can run Python code to perform calculations, analyze data, or do anything else code is good for. Crucially, we'll build this inside a "sandbox"—a safe, restricted environment. We don't want our agent accidentally deleting files on our computer! We'll limit the functions it can use to keep things secure. - Dataset Loader (
load_builtin_dataset): A specialized tool for data science tasks. It can load classic datasets (like the Iris or Wine datasets) and instantly get a summary, making it easy for the agent to answer questions about them. - Vector Retriever (
tool_vector_retrieve): This is the agent's personal memory. It connects to that local knowledge base we created earlier. When a question is about our system's own concepts (like "What is MCP?"), it can use this tool to find the answer internally instead of searching the web.
With these four tools, our agent has a nice, well-rounded skillset. It can look things up online, perform calculations, inspect data, and consult its own memory.
The Central Hub: The MCP-Style Tool Server
Now that we have our tools, we need a way to manage them. We can't just leave them lying around. We need a central "toolshed" or a server that knows what every tool is, what it does, and how to run it.
This is where our MCPToolServer comes in. It's a straightforward class that does two main things:
- It keeps a registry of all available tools. We "register" each of our four tools with the server, handing it the tool's function and its
ToolSpec(the ID card we defined earlier). - It handles requests to use a tool. When the agent decides to use a tool, it sends a request to the server. The server finds the right tool, runs it with the provided arguments, and then neatly packages up the result—whether it worked or not.
This server-based approach is great because it decouples the tools from the agent. The agent doesn't need to know the messy implementation details of how to run Python code safely; it just needs to know how to ask the server to do it. This makes our system clean, modular, and easy to expand later on.
The Real Magic: Our Hybrid "Tool Router"
Here’s where things get really interesting. We have a server with four great tools. The naive approach would be to show all four tools to the agent for every single task. But as we discussed, that's a recipe for confusion.
Instead, we're building a HybridMCPRouter. This is the smart manager that decides which tools are actually relevant for the job at hand. The "hybrid" part is key—it uses a two-step process to make smart, efficient decisions.
Step 1: The Quick Heuristic Scan
First, the router does a quick, cheap, and simple check. It scans the user's task for keywords.
- Does the task mention "calculate," "plot," or "Python"? The
python_exectool gets a point. - Does it mention "search," "latest," or "news"? The
web_searchtool gets a point. - Does it mention "dataset," "iris," or "columns"? The
dataset_loadertool gets a point.
This is a fast, rule-based way to create a "shortlist" of the most likely candidates. It’s not perfect, but it instantly narrows down the options from four to maybe two or three.
Step 2: The LLM's Expert Opinion
Next, the router takes this shortlist and presents it to a Large Language Model (LLM). It gives the LLM the original task, the full list of all available tools, and the heuristic shortlist. Then it asks a simple question: "Based on all this, which tools should we actually expose to the agent? Be minimal. Be smart."
The LLM acts as the final decision-maker. It can understand the nuance of the task in a way that simple keyword matching can't. For example, a task might mention "Python" but be a conceptual question where a code executor isn't actually needed. The LLM can catch that.
The router then spits out a RouteDecision, which includes the final list of selected tools and a rationale explaining its choice. This two-step process gives us the best of both worlds: the speed of heuristics and the intelligence of an LLM.
Putting It All Together: The Routed Agent in Action
We've built all the pieces: the tools, the server, and the router. Now, let's assemble our RoutedAgent. This is the "chef" who will actually perform the work.
Here’s how the agent operates, step-by-step:
- Get the Task: The agent receives a task from the user, like "What is dynamic capability exposure?"
- Consult the Router: The agent doesn't look at the main toolshed. Instead, it asks the
HybridMCPRouterfor guidance. The router runs its hybrid process and comes back with a decision: "For this task, you only need thevector_retrievetool. Here’s why..." - Discover Exposed Tools: The agent now knows it only has one tool available to it. Its world just got a lot simpler.
- Make a Plan: The agent, now only seeing the
vector_retrievetool, thinks about how to solve the task. It creates aPlanOutput, saying: "Yes, I need to use a tool. My plan is to callvector_retrievewith the query 'dynamic capability exposure'." - Execute the Plan: The agent sends the tool call to the
MCPToolServer. The server runs the tool and returns the result—a snippet of text from our local knowledge base. - Synthesize the Final Answer: Finally, the agent takes everything it has—the original task, the router's decision, its own plan, and the tool's output—and uses it all as context to generate a final, helpful answer for the user.
This entire workflow is incredibly transparent. At each stage, we can see exactly what decisions were made and why. When we run a few demo tasks, you can see it in action. For a question about MCP, it uses the local retriever. For a question about recent news, the router correctly exposes the web search tool. For a data analysis question, it exposes the dataset loader and Python executor. It just works.
Why Does This Approach Matter?
So, we've built a pretty cool system. But what's the big takeaway?
This MCP-style, routed architecture is more than just a fun project. It represents a more mature way of building AI assistants. By dynamically controlling which tools the agent sees, we gain several huge advantages:
- Improved Focus and Accuracy: The agent is less likely to get confused or make a mistake when it only has a few relevant options to choose from.
- Enhanced Safety and Control: You can create policies to prevent the agent from accessing sensitive or dangerous tools (like a code executor or a database write tool) unless the task absolutely requires it.
- Better Interpretability: When something goes wrong, it's much easier to debug. You can look at the router's decision and the agent's plan to see exactly where the logic failed.
- Greater Efficiency: The agent's prompts are smaller because they don't include descriptions for a dozen irrelevant tools. This saves tokens and reduces latency.
This is how we move from unpredictable AI novelties to reliable, production-grade systems. It’s about adding a layer of intelligent control that guides the agent toward the right solution, every time. The foundation we've built here is a fantastic starting point for creating even more sophisticated, capable, and trustworthy AI assistants.




