Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

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.

Wrapper code for every service · from the MCP Agentic AI Crash Course With Python · 4:23 to 9:26

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.

python
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.

python
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.

Example
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"]))

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 schemaYou, next to each functionThe SDK, from your type hints
Reuse across appsRewritten per applicationOne server, any MCP host lists it
Keeping schema in stepManual, easy to forgetRebuilt from the function each time
Calling conventionEach app's own dispatchOne 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.
Watch out. A hand-written schema that drifts from its function is a bug a model cannot see: it sends arguments that match the schema and your function rejects them. Moving the tool into a server, the next lessons' work, removes the drift because the schema is rebuilt from the function.
Try it yourself
  • Add a second function and its schema by hand. Count the places you would change if order_id became 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.