Tools without MCP
Tool calling without MCP is the hand-written pattern where each application describes your function and dispatches to it in its own format.
Last updated: 29 Sep, 2026 · MCP 2.2
A model cannot run code; it can only ask for a function by name with arguments. In the previous lesson you installed the SDK that removes the glue this pattern needs. Here you write that glue once by hand, so you can see what MCP takes off your hands.
The MCP crash course draws the problem first. An LLM on its own cannot answer a question about recent news, so it was connected to outside services, such as Tavily search, DuckDuckGo search, Arxiv and Wikipedia, each through wrapper code the developer wrote with that service's package. Whenever a service changed, its wrapper had to change too. With MCP, the service provider runs an MCP server and keeps it up to date, and the application needs only an MCP client, the way one USB-C cable connects a phone to many devices.
The smallest version of that wrapper code has one function, the description a model API needs for it, and the dispatch that runs it.
The function and its schema
Start with a function a support model should be able to use, and the description a model API needs for it. The description is a JSON Schema: a JSON statement of the arguments, saying order_id is a required string.
def lookup_order(order_id):
orders = {"A17": "blue mug, shipped", "B42": "desk lamp, processing"}
return f"Order {order_id}: {orders[order_id]}."
lookup_order_schema = {
"name": "lookup_order",
"description": "Look up an order by its id and say where it is.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
}Dispatching what the model asked for
A model answers with a name and arguments. Your application looks the function up by name and calls it with those arguments.
model_asked_for = {"name": "lookup_order", "arguments": {"order_id": "A17"}}
functions = {"lookup_order": lookup_order}
function = functions[model_asked_for["name"]]
print(function(**model_asked_for["arguments"]))Running the hand-written dispatch
The pieces in one file, with the schema kept next to the function it describes. Save it as dispatch.py and run it.
def lookup_order(order_id):
orders = {"A17": "blue mug, shipped", "B42": "desk lamp, processing"}
return f"Order {order_id}: {orders[order_id]}."
lookup_order_schema = {
"name": "lookup_order",
"description": "Look up an order by its id and say where it is.",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
}
model_asked_for = {"name": "lookup_order", "arguments": {"order_id": "A17"}}
functions = {"lookup_order": lookup_order}
function = functions[model_asked_for["name"]]
print(function(**model_asked_for["arguments"]))Order A17: blue mug, shipped.
What the dispatch cost you
- The schema was hand-written next to the function and has to be kept in step with it by hand.
- It fits one application. A second application needs the same function described in its own format.
- The wiring repeats. Ten tools across four applications is forty pieces of glue to keep correct.
Hand-written glue vs an MCP server
| By hand (this lesson) | With an MCP server | |
|---|---|---|
| Who writes the schema | You, next to each function | The SDK, from your type hints |
| Reuse across apps | Rewritten per application | One server, any MCP host lists it |
| Keeping schema in step | Manual, easy to forget | Rebuilt from the function each time |
| Calling convention | Each app's own dispatch | One standard call over the protocol |
When you still write tools by hand
- A one-off script where a single model API calls a single function.
- Learning what a model API expects before a server hides it, as here.
Related
- Previous: Installation and setup
- Next: MCPServer quickstart
- Reference: MCP server concepts
- Add a second function and its schema by hand. Count the places you would change if
order_idbecame an integer. - Ask for
"order_id": "Z9"and read the error. - Remove
"required"from the schema. What would stop a model from calling the tool with no arguments?
You understood something today that you didn't yesterday.