The other side of Agentic AI

An agent's utility is capped by its environment interface rather than just its reasoning capabilities.

Tools, MCP, and why it matters

Introduction

Iron Man and JARVIS. James Bond and Q. What do they have in common? They’re both dynamic duos — capable individuals navigating complex situations with the help of powerful tools and clever gadgets. It’s not just about raw talent; it’s about having the right support system behind the scenes to get the job done.

Similarly, no matter how powerful your LLM model is, it will always face limitations if confined to its own domain. One of the most common constraints? The knowledge cutoff date of the model’s training data. Without a way to tap into real-time tools, systems, or information, even the smartest models can only do so much.

To get around this limitation, one powerful solution is to equip the model with tools — like the ability to search the web when it can’t find relevant info in its own knowledge base. Think of it as giving your AI agent access to Q Division’s latest gadgets. This approach comes with several key benefits:

  • No need to retrain the model — it just needs to know how to use the web search tool
  • The tool can be updated independently of the model
  • Additional tools can also be added easily as the need arises

It’s not a new concept — remember the good “old” days (circa 2023) when we just called them Application Programming Interfaces (APIs)? Well, times have changed. As LLM apps get snazzier and start showing up in tuxedos (read: become agents), APIs need a glow-up too — enter the Model Context Protocol (MCP).

What is the Model Context Protocol (MCP)?

MCP is an open protocol from Anthropic that standardizes how applications provide context to LLMs. In their words:

Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.
MCP Architecture

Prior to MCP, there were other frameworks, such as OpenAI’s Function Calling API, that also enabled models to call external functions or APIs to retrieve data or perform tasks beyond their pre-trained knowledge. While the core functionality of MCP is quite similar, its primary aim is to be a broader, standardized protocol designed to be seamless and easy to use. MCP expands on the same concept and further builds on it by offering a more flexible and universal framework for integrating external systems into AI workflows.

MCP in practice

1. The MCP-less setup

As described previously, the agentic setup can also be achieved without MCP by incorporating the tool’s code into the client application itself. This essentially means that the tool is closely coupled with the application. Consider this approach if:

  • There is no need to replicate or share the tool
  • The tool is tied to the lifecycle of the application (does not need to be available when the application is not)
  • The tool performance is an important consideration (i.e. direct integration to have as little overheads as possible)

For sample code you can take a look at the OpenAI Docs.

2. The Local MCP setup

This is similar to the above setup, but with the inclusion of the MCP Server and Client components. The tool can either be locally defined within an MCP Server, or code pulled remotely from a MCP Repo — very much like pulling code from GitHub. Note that the Server terminology in this case does not mean that it serves the tool as an endpoint; it’s more akin to importing a package in Python. Communication between the client and server in the local context can be via stdio. Consider this approach if:

  • Your tool requirements may change over time
  • Your tool requirements are generic or public domain (e.g. ability to web-search, or find files in computer)
  • The tools need to be updated more frequently than the application

Calling the MCP Server with stdio and listing the tools available:

async def connect_to_server(self, server_script_path: str):
session: Optional[ClientSession] = None
exit_stack = AsyncExitStack()

server_params = StdioServerParameters(
command="python",
args=[server_script_path],
env=None
)

stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

await self.session.initialize()

# List available tools
response = await self.session.list_tools()
tools = response.tools
print("\nConnected to server with tools:", [tool.name for tool in tools])

Then whenever the LLM response includes a tool_call, the tools can be called using

result = await session.call_tool(tool_call.function.name, args)

3. The Remote MCP setup

For this setup, the MCP Server has been moved into the external environment and served with a server application (e.g. Starlette). The client environment no longer needs to have the tool code but essentially just uses it like an API. Communication is done using SSE and the tool is executed in the external environment before returning the results. This approach can be beneficial if:

  • You don’t want to share your tool code (e.g. you can predict the next lottery numbers)
  • Your tool requires significant compute resources that you don’t want to require your clients to handle
  • You want the clients to be lightweight

Connecting to MCP Server via SSE:

async def test_connect_to_sse_server(server_url: str):
async with sse_client(url=server_url) as streams:
async with ClientSession(*streams) as session:
await session.initialize()
response = await session.list_tools()
print(f"Connected to server {server_url} with tools:", [tool.name for tool in response.tools])
return server_url, response.tools

Creating a simple subtraction tool in MCP:

from fastmcp import FastMCP
mcp = FastMCP("Demo")

@mcp.tool()
def subtract(a: int, b: int) -> int:
return a - b

Serving the MCP Server via Starlette:

from fastmcp import FastMCP
from starlette.applications import Starlette
from mcp.server.sse import SseServerTransport
from starlette.requests import Request
from starlette.routing import Mount, Route
from mcp.server import Server
import uvicorn

def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette:
sse = SseServerTransport("/messages/")
async def handle_sse(request: Request) -> None:
async with sse.connect_sse(
request.scope,
request.receive,
request._send, # noqa: SLF001
) as (read_stream, write_stream):
await mcp_server.run(
read_stream,
write_stream,
mcp_server.create_initialization_options(),
)
return Starlette(
debug=debug,
routes=[
Route("/sse", endpoint=handle_sse),
Mount("/messages/", app=sse.handle_post_message),
],
)

if __name__ == "__main__":
mcp_server = mcp._mcp_server
starlette_app = create_starlette_app(mcp_server, debug=True)
uvicorn.run(starlette_app, host=’0.0.0.0’, port=8080)

Example Setup

You can see that the client application manages to connect to the 2x local MCP servers and the remote server, and is able to obtain the list of tools provided by each.

Query: what is 3 + 2
['add', 'multiply', 'subtract', 'divide', 'tool_404']
tool_calls
=====================================
ChatCompletionMessageToolCall(id='call_bRqOO5t2BQ7IqjmE0uD8UUio', function=Function(arguments='{"a":3,"b":2}', name='add'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='5', annotations=None)] isError=False
=====================================
NEXT ROUND --- 3 + 2 equals 5.
=====================================

[Calling tool add with args {'a': 3, 'b': 2}] --- [Results 5]
3 + 2 equals 5.

Given a simple query of “what is 3+2”, the model correctly identifies it as an addition problem and chooses to use the add tool in its toolset. It then uses the tool result for the final answer of 5.

Let’s try a more complicated question, like “what is (10+2)/6*3–4+5/5”

Query: what is (10+2)/6*3 - 4 + 5/5
['add', 'multiply', 'subtract', 'divide', 'tool_404']
tool_calls
=====================================
ChatCompletionMessageToolCall(id='call_ckodB6cU3GoMx2ADTR4GqqhO', function=Function(arguments='{"a":10,"b":2}', name='add'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='12', annotations=None)] isError=False
=====================================
NEXT ROUND --- None
=====================================
ChatCompletionMessageToolCall(id='call_ptUceSfBHDaDKe150eOOCfU9', function=Function(arguments='{"c":12,"d":6}', name='divide'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='2.0', annotations=None)] isError=False
=====================================
NEXT ROUND --- None
=====================================
ChatCompletionMessageToolCall(id='call_GKTM7nspK1PZhj603gARFUHn', function=Function(arguments='{"a":2,"b":3}', name='multiply'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='6', annotations=None)] isError=False
=====================================
NEXT ROUND --- None
=====================================
ChatCompletionMessageToolCall(id='call_1gbnt3WKBigiLNRjVgG4GD26', function=Function(arguments='{"a": 6, "b": 4}', name='subtract'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='2', annotations=None)] isError=False
ChatCompletionMessageToolCall(id='call_OaW8b9cYcuxvBkgSJ6EveW4t', function=Function(arguments='{"c": 5, "d": 5}', name='divide'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='1.0', annotations=None)] isError=False
=====================================
NEXT ROUND --- None
=====================================
ChatCompletionMessageToolCall(id='call_CsM9BjDKs8CxZSSQ3Dx4yzPd', function=Function(arguments='{"a":2,"b":1}', name='add'), type='function')
....................
Tool result >>> meta=None content=[TextContent(type='text', text='3', annotations=None)] isError=False
=====================================
NEXT ROUND --- The result of the expression (10 + 2) / 6 * 3 - 4 + 5 / 5 is 3.
=====================================

[Calling tool add with args {'a': 10, 'b': 2}] --- [Results 12]
[Calling tool divide with args {'c': 12, 'd': 6}] --- [Results 2.0]
[Calling tool multiply with args {'a': 2, 'b': 3}] --- [Results 6]
[Calling tool subtract with args {'a': 6, 'b': 4}] --- [Results 2]
[Calling tool divide with args {'c': 5, 'd': 5}] --- [Results 1.0]
[Calling tool add with args {'a': 2, 'b': 1}] --- [Results 3]
The result of the expression (10 + 2) / 6 * 3 - 4 + 5 / 5 is 3.

The model broke down the tasks into the steps given the tools it has available and calls them over multiple iterations until the final answer is reached, resolving each tool call and incorporating the results into the next step.

This example setup demonstrates the ease of using MCP to incorporate tools into agentic AI applications as well as the modularity and versatility of this approach.

Applicability in enterprises

Why do we need our own MCP servers

Many enterprises have proprietary systems with proprietary data that is not generally available from open sources. Even if the underlying databases are the same, there may be specialized pipelines or processes that transform, clean, and enrich the data before it is shared or utilized within the organization. Furthermore, access could be locked behind enterprise subscription keys or other authentication methods.

In situations where it is not feasible or desirable to open-source the tool code, having a centrally hosted MCP Server with an integrated tool execution environment can offer several advantages, such as -

  • Allowing the organization to maintain strict control over its proprietary tools and data processing logic by centralizing the execution of tools on a secure server.
  • Abstracting away some of the complexities associated with managing and deploying proprietary tools across various client environments by using the MCP Server as a single point of control and execution.
  • Offloading the execution of resource-intensive tools to server infrastructure and using standardized protocols such as SSE for communication ensures that data is transmitted reliably and in real-time.
  • Serve as a trusted resource for tools in contexts where the integrity and authenticity of data are paramount (e.g. trusted place where you can get shared Government real-time traffic data).

Where can it be useful?

The decision to use MCP or traditional APIs depends on the specific requirements of the application. MCP is well-suited for scenarios where dynamic, context-aware interactions with multiple tools and data sources are needed, particularly in agentic AI applications. On the other hand, traditional APIs are ideal for simpler, well-defined tasks that benefit from direct, efficient communication and the maturity of the API ecosystem.

When should you not use it?

While useful, there are situations where you may not need a central MCP server, such as:

  • When the tool function is generic, does not require significant resources, and can be easily achieved with open-source versions, such as web searching
  • When data needs to be kept local
  • When latency is important

Current challenges

MCP is a fairly new protocol which also means that it has not been battle tested for long and is lacking some features of its more mature cousin, the REST API. Some notable challenges with MCP currently include:

Authorization and Authentication

This blog from Christian Posta summarizes the issue. Using our example setup as reference, if we need to implement Oauth in each MCP server, that would mean that our example client would need 3 different authentication tokens, and much more if we connected to more servers.

Issuing tokens is one thing, managing and tracking them is a whole other beast altogether. That is partly why identity providers exist in the first place.

Provisioning ad-hoc infrastructure

Depending on the complexity of the tools hosted, the underlying infrastructure required may not be trivial. If so, how should the infrastructure be dynamically and efficiently provisioned on demand, or would it have to be online 24/7. There would be a need to integrate with a workflow or orchestration framework, perhaps like Prefect, to achieve this.

Limitations of a protocol

Protocols inherently introduce overhead to provide features such as reliability and security. As applications grow in complexity and size, the overhead may become significant. Whilst the majority of the players in the space have opted to adopt it for now, there is always the chance that something different will come upon which the majority switches to.

Conclusion

In the increasingly agentic AI world of today, the MCP offers many benefits for enterprises that have data and systems to be exposed to agents in a manageable and consistent manner. In the public sector, agencies can leverage it to share data with AI agents while still keeping the internals private, or to serve as a trusted resource for tools in contexts where the integrity and authenticity of data are paramount.

If you would like to find out more about agents, take a look at our Agentic AI Primer!