Practice
One request, three boundaries, and one calculator behind the door
A local Python explainer about how trust and execution responsibility move from direct code to model requests to MCP.
AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy
The request looked innocent enough: “What is one hundred twenty-five multiplied by forty-eight?”
That sentence is useful because it is boring. It has one answer, one operation, and no need for a committee. Boring examples are excellent for architecture because they leave fewer places for hand waving to hide.
I built this to make the boundary visible. The same request crosses three different execution shapes: plain code calling a function, model output asking host code to perform a known action, and MCP adding discovery plus protocol-based invocation. The arithmetic stays put because the point is not the math. The point is who gets to decide, who actually runs, what gets trusted, and what still needs to be checked before anything useful happens.
At a glance
The direct call boundary
The model-request boundary
The validation boundary that earns the call
The MCP protocol boundary
Why this exists#
Some demos treat an LLM like a calculator with a poetry habit, which is how a simple arithmetic request becomes an architecture discussion. This one has a narrower reason: explain how trust and execution responsibility move when the same request crosses direct code, model-produced structure, and MCP.
- The itch: A tool call is not one thing. A caller invoking its own function, a model returning a request for a host to act on, and MCP exposing discoverable tools all move the trust boundary to different places. Pretending they are the same is how demos become vague with confidence.
- Why I built it: To explain a concept: how trust and execution boundaries change across a plain direct tool call, an LLM-produced request that host code validates and executes, and an MCP protocol boundary where tools can be discovered and invoked.
- What you get: A local pattern you can copy: keep deterministic behavior in normal Python, treat model output as untrusted data, let host code own authorization, and use MCP for discovery and invocation without handing the toolbox to the model.
- Who this is for: Python developers, AI-app builders, and MCP-curious engineers who need a crisp way to explain who decides, who executes, what is trusted, and what must be validated. It also helps if you have seen one too many demos where the model is asked to do the part a function could do before coffee.
- Who can skip it: If your architecture already treats model output like untrusted airport luggage, this may mostly confirm your excellent lack of drama.
With that itch scratched, the rest of the story follows the same request as the boundary moves and the calculator quietly refuses to become a platform.
The plan#
I set out to explain the same concept three ways. First, plain code can call a deterministic function directly when the caller already knows the operation and arguments. Second, a model can turn natural language into a structured request, but the host still decides whether that request is allowed and the host still executes it. Third, MCP can add discovery and standard tool invocation, but the host must still enforce what may run.
One accuracy note before the arrows start multiplying: the repository’s second path does not use a native LLM tool-call envelope. It uses local Ollama to return a JSON intent and parameter request. That still demonstrates the owner’s boundary: the model asks in structured form, while the host decides and runs. The envelope is different. The trust problem is the same, which is rude but convenient.
You will want Python 3.10 or later, a local Ollama setup if you want the live model path, and enough MCP vocabulary to survive the phrase “tool discovery” without requesting a committee. No Azure resources, paid services, cloud identities, or public endpoints are needed.
Here is what I thought the shape should look like before the code had a chance to object.

The important part is not that there are three arrows. The important part is that every arrow ends at the same deterministic implementation. If that changed, the demo would prove only that I had found three places to hide bugs.
| Boundary | Who decides | Who executes | What is trusted | What must be validated |
|---|---|---|---|---|
| Direct Python call | Caller code chooses the calculator and supplies arguments | The caller’s process invokes deterministic Python | The local call path, after normal input checks | Operation allowlist, numeric inputs, and divide-by-zero |
| Model request to host code | Local Ollama proposes structured intent and parameters | Host code validates the request and calls the calculator | Nothing in the model response until validation passes | JSON shape, exact fields, intent, confidence, and types |
| MCP protocol boundary | Host code maps validated intent to an approved MCP tool | The MCP client calls the server tool, then Python runs it | Discovered tool metadata, never a model-supplied tool name | Required tools, host allowlist, arguments, and tool errors |
1. The direct call boundary#
I started with the least glamorous version because it has the best manners. The caller’s own code already knows the operation and the two numbers, so it asks for three fields and calls the calculator directly.
I started with the least glamorous version because it has the best manners.
What happened was exactly what should happen: no Ollama, no MCP, no ambiguity, and no
invitation for a model to improvise. The caller decides to use the calculator. The caller’s
process executes the function. The trusted part is small: local code and explicit arguments
after validation. The source tests confirm the prompt count, the
Ollama calls: 0 output, and the final Python result: 6000.
Here is the first version doing exactly what it should do, which is almost offensively plain. The caller already knows the operation and operands, so Python calculates 6000 and Ollama gets the afternoon off.

The change that mattered was not clever. I kept arithmetic in one deterministic function and made everything else prove it deserved to call that function. That proof starts here with an allowlist, numeric checks, and the usual refusal to divide by zero because even demos deserve standards.
def calculate(operation: str, left: object, right: object) -> dict[str, Any]:
"""Perform one supported arithmetic operation without dynamic evaluation."""
if not isinstance(operation, str) or operation not in SUPPORTED_OPERATIONS:
raise ValueError("Unsupported operation.")
Complete snippet: Deterministic capabilities with a strict request contract
2. The model-request boundary#
The next version allowed the user to say the request like a person instead of a form. In the owner’s terms, this is the boundary where the model returns a request for the host to act on. In this codebase, the request is JSON from local Ollama rather than a native tool-call object: intent, confidence, and parameters. Do not answer the request. Do not calculate. Do not wander off and become a spreadsheet.
The next version allowed the user to say the request like a person instead of a form.
The model decides nothing final. It proposes a domain request. The host decides whether the request is acceptable and then invokes deterministic Python. The surprise, if you can call it that, is that the schema helped but did not remove the need for suspicion. The tests reject fenced Markdown, invalid JSON, missing fields, unknown intents, bad confidence values, and wrong parameter types. The model response is still text from a model until Python validates it.
This is the part where the model is useful without being promoted to treasurer. It turns English into a validated calculator request, then host code decides whether the same Python function may run.

The piece that made this path work was the payload. It asks for structure, disables streaming and thinking, sets temperature to 0, and still hands the result to the same request contract before anything can execute.
return {
"model": MODEL_NAME,
"stream": False,
"think": False,
"format": INTENT_RESPONSE_SCHEMA,
"options": {"temperature": 0},
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_input},
],
}
Complete snippet: Local Ollama intent extractor with schema output
For the sentence “What is one hundred twenty-five multiplied by forty-eight?”, the model’s
job is to propose data with the calculator intent, multiply operation, and the two
numeric operands. Then Python parses it, validates it, and executes normal code. If Ollama returns fenced
Markdown, missing fields, an unknown intent, or a bonus parameter it found under the sofa,
the request fails safely. The validated request can ask for execution. It cannot grant
itself permission.
3. The validation boundary that earns the call#
Once the model could translate the sentence, the tempting mistake was to let the translated shape drift into “close enough.” That is where demos start smiling too much. I wanted the contract to behave more like a bouncer than a greeter.
Once the model could translate the sentence, the tempting mistake was to let the translated shape drift into "close enough." That is where demos start smiling too much.
The host validates top-level fields, allowed intents, confidence, exact parameters, numeric
types, and calculator operations. It rejects Boolean numbers. It rejects extra parameters.
It rejects a model-supplied tool field, which is a polite way of saying the model is not
allowed to rummage through the toolbox. This is the responsibility shift that matters: the
model can produce a request, but host code owns authorization.

The implementation is intentionally fussy at the boundary where fussy is useful.
if intent == "calculator":
require_exact_fields(parameters, {"operation", "left", "right"}, intent)
operation = require_string(parameters, "operation", intent)
if operation not in SUPPORTED_OPERATIONS:
raise RequestValidationError(
"Calculator operation must be add, subtract, multiply, or divide."
)
Complete snippet: Deterministic capabilities with a strict request contract
4. The MCP protocol boundary#
The third path added MCP because more than one host may want to discover and invoke the same capability. MCP adds a protocol boundary: the client can list tools, read schemas, and call a server tool through a standard interface. That is useful. It still does not make arithmetic more interesting, despite the best efforts of architecture diagrams everywhere.
The third path added MCP because more than one host may want to discover and invoke the same capability.
The obstacle was tool choice. If the model can say not only “calculator” but also “call this tool name with these arguments,” the boundary has moved to the wrong place. So the host owns the mapping from domain intent to MCP tool name. The model proposes domain intent. The host chooses the allowed MCP tool. The MCP client discovers the server’s tools and verifies the required tool exists before invocation. The MCP adapter delegates to existing Python.
The third run adds the protocol boundary. That sounds grand, but the output is refreshingly stubborn: the approved MCP tool is calculate, the transport is in memory, and the Python result is still 6000.

MCP_CAPABILITIES = {
"calculator": "calculate",
"email_validator": "validate_email",
"create_support_ticket": "create_support_ticket",
}
Complete snippet: Host-owned routing for direct and MCP execution
The MCP server is thin enough to be almost disappointing, which is the point.
@mcp.tool()
def calculate(operation: str, left: int | float, right: int | float) -> dict[str, Any]:
"""Perform basic arithmetic using deterministic Python code."""
return deterministic_calculate(operation, left, right)
Complete snippet: Thin MCP server adapters over deterministic Python tools
The moment it worked#
The useful proof was not a benchmark chart or a heroic terminal transcript. It was watching
the same request land in the same calculator through three different boundaries and still
print 6000.
The structured path printed that there were zero Ollama calls. The direct natural-language
path printed a validated JSON request and then Python result: 6000. The MCP path printed
Approved MCP tool: calculate, Implementation: tools/calculator.py, and the same result.
The tests captured those outputs and the source unit run reported 108 successful tests with
5 skipped integration tests.

That was the satisfying part. The implementation never changed. The decision point, execution path, and validation duties changed.
What I would tell you before you try this#
- Start with one request and one deterministic implementation. If the no-model path does not work, MCP will not make it wiser.
- Make the zero-model path work first. It gives you a control case and removes the fog.
- Use the model only to produce a structured request. Interpretation is a job; execution is a different job.
- Treat model output as untrusted input until validation passes. Friendly JSON is still input from outside the trust boundary.
- Reject extra fields instead of ignoring them. Unknown fields are where surprises begin rehearsing.
- Keep tool-name mapping in host code, not in the prompt. Prompts suggest; hosts authorize.
- Make MCP adapters thin enough to be boring. Boring adapters are easier to test and harder to misunderstand.
- Test that invalid model output never reaches execution. The failure path is part of the feature.
- Measure local boundary costs carefully. One laptop is useful evidence, not a benchmark paper.
- Keep the demo humble. The software industry has enough confident calculators already.
Put it together#
If you want to build your own version, keep the epilogue as small as the demo. This solution runs locally. It does not need Azure, paid services, cloud identities, or public endpoints. The cloud bill is therefore $0.00. If you use a local Ollama model, there is no model token charge either. Your laptop still uses electricity, but that invoice arrives through more traditional channels.
Validated local versions for this draft:
| Tool or package | Version or setting | Purpose |
|---|---|---|
| Python | 3.14.5 used for validation; 3.10 plus | Runs the snippets |
mcp package |
2.0.0 | Provides the MCP server adapter |
| Ollama | 0.32.5 | Local intent extraction |
| Model | qwen3:8b |
Local JSON intent proposal |
Minimum resources and monthly cost:
| Resource or SKU | Minimum needed for this demo | Estimated monthly cost |
|---|---|---|
| Azure resources | None | $0.00 |
| Hosted model tokens | None with local Ollama | $0.00 |
| Local Ollama and Python | Owner machine | No cloud charge |
Use the snippets in this order:
- Save the deterministic capabilities as
deterministic_capabilities.py. - Save the Ollama intent extractor as
ollama_intent_extractor.py. - Save the MCP server adapter as
mcp_tool_server.py. - Save the host routing module as
host_authorized_routing.py. - Run a structured calculator request directly and confirm
6000. - Run an Ollama-backed intent extraction and confirm the same request becomes the same validated JSON shape.
- Run the host routing path with an MCP client and confirm the approved tool is
calculate.
The demo proves a small point with sharp edges: every boundary needs a named owner. Direct code can decide and execute for itself. Model output can ask, but host code decides and runs. MCP can discover and invoke tools through a protocol, but the host still guards the door. Once that clicked, MCP stopped looking like magic and started looking like a useful door with a very ordinary calculator behind it.
Remember these
From this post
Snippet
Deterministic capabilities with a strict request contract
A complete Python module that validates model output as data, then runs approved normal code for calculator, email syntax, and mock support-ticket requests.
Snippet
Host-owned routing for direct and MCP execution
A complete Python unit that maps validated domain intent to approved direct functions or approved MCP tool names.
Snippet
Thin MCP server adapters over deterministic Python tools
A complete MCP server module that exposes approved deterministic capabilities without moving business logic into the MCP layer.
Snippet
Local Ollama intent extractor with schema output
A complete Python unit that asks local Ollama for one JSON intent object, then validates the result before execution can happen.
Architecture
Zero-token execution boundary for local AI tool demos
A reusable local architecture that lets an LLM interpret natural language while deterministic Python and optional MCP boundaries own execution.