Generate MCP from a Python file or notebook
For a multi-module repository, use REST/MCP from a repository or the repository Quickstart. This guide takes one Python file or notebook as its input.
Install Apizr, then generate an MCP server from your selected Python functions. You can call it from an MCP client or an AI assistant. Generation reads your source without running it; the server runs your functions when a client calls them.
Watch: Generate MCP tools and call them with Python · 5:09
Expose color conversions from TheAlgorithms/Python, discover the tools and handle their results with the official MCP Python client.
English · Open on YouTube · Alien6 Studio. Use the written steps for the current release; a recording may show an earlier version.
Inspect and generate
The TypedDict model example
works with MCP as well as REST. predict advertises the same closed, nested object
in inputSchema. Calling it with
{"payload":{"customer_id":"c","features":{"age":2,"score":3}}} returns
isError=false, structured content {"result":6.0} and matching JSON text.
Missing, extra or invalid nested fields produce isError=true, no structured
content and the text Invalid tool arguments. Return annotations remain
descriptive; generated tools do not advertise outputSchema.
Given pricing.py:
from typing import Literal
def calculate(prices: list[float], /, *, currency: Literal["EUR", "USD"] = "EUR"):
"""Add the supplied prices and retain their currency."""
return {"total": sum(prices), "currency": currency}
Inspect its eligibility, then generate into a new directory:
apizr inspect pricing.py
apizr generate mcp pricing.py --output-dir ./generated-mcp
One .ipynb input works too. Dotted logical identities and selection are explicit:
apizr generate mcp pricing.py --module-name project.pricing \
--select calculate --output-dir ./generated-mcp
Selection accepts comma-separated names or full capability IDs. Unknown names and non-eligible selections fail. Without selection, any conditional, unsupported or ambiguous callable declaration prevents generation. There is no unsafe override. Use an empty physical directory without symlink components; existing user files are never overwritten.
Run trusted source
Starting a direct-mode server imports and executes the bundled source; a governed server defers that import to its worker on each call. Run only source and dependencies you trust. Readiness is not a security sandbox or an execution approval.
cd generated-mcp
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python server.py --transport stdio
For a local HTTP endpoint instead:
.venv/bin/python server.py --transport streamable-http --port 8000
Connect to http://127.0.0.1:8000/mcp. Both transports use the official Python MCP
SDK v2 and target protocol 2026-07-28. The same generated files serve both. The
server binds locally by default; exposing it remotely requires your deployment's
access-control policy.
The runtime environment needs no Apizr installation. requirements.txt declares
mcp>=2.2,<3, AnyIO and Uvicorn. Source dependencies are not inferred or bundled.
Call a Tool with the official SDK
With the HTTP server running, use the official SDK client in another process:
import asyncio
from mcp import Client
async def main():
async with Client("http://127.0.0.1:8000/mcp") as client:
print((await client.list_tools()).tools)
result = await client.call_tool("calculate", {"prices": [10, 20]})
if result.is_error:
raise RuntimeError(result.content)
print(result.structured_content) # {"total": 30.0, "currency": "EUR"}
asyncio.run(main())
The omitted currency uses Python's default. Explicit null is accepted only for nullable or unconstrained inputs. Extra fields and invalid types return a Tool error. Positional-only inputs remain object fields in MCP; the adapter reconstructs the Python call. Supplying a later optional positional-only field requires its preceding positional-only fields, exactly as in REST.
Object results stay unchanged. Other finite JSON results use an object envelope:
25.0 becomes {"result": 25.0}, a list becomes {"result": [...]}, and None
becomes {"result": null}. Read the value from result.structured_content["result"]
for these functions. Python result tuples, including nested tuples, are recursively
converted to JSON arrays in order: (1, 2) becomes {"result": [1, 2]}, and
{"support": ("61.8%", 10.674)} becomes {"support": ["61.8%", 10.674]}.
This applies to direct, local-process and OCI modes, including repository bundles.
The text content serializes the same object. This correction
requires regenerating older bundles that emit bare values, which strict clients
can reject. See the result contract.
Sync and async functions work. After tuple normalization, results must contain
only finite JSON scalars, arrays and dictionaries with string keys. NaN, infinity,
arbitrary objects and sets remain rejected even when nested. Tuples are normalized
at the result boundary; JSON inputs and Python argument reconstruction keep their
existing rules. An unannotated return remains unknown during static analysis;
declared return annotations do not constrain runtime results. Unsupported results and
unexpected exceptions produce Tool execution failed without internal exception
details. The server does not assign safety annotations because effects are unknown.
Choose direct or governed execution
Direct mode remains the default. To opt into a bounded fresh worker for every request/Tool call, generate a separate bundle with an explicit policy:
apizr generate mcp pricing.py --execution-policy examples/policies/local-default.json \
--output-dir .output/mcp-governed
Use that example policy from the Apizr checkout, or create policy.json containing
{} and pass its path. Both Python and notebook inputs support the option.
| Behavior | Direct (default) | Governed (opt-in) |
|---|---|---|
| Source import | In server at startup | Only in a fresh worker on each call |
| Globals / mutable defaults | Persist between calls | Reset each call |
| Wall timeout | No process timeout boundary | Worker termination on policy deadline |
| Worker protocol | Existing in-process invocation | Bounded input/output |
| Environment | Server environment | Clean or explicitly allowlisted |
| Filesystem/network sandbox | None | None |
Both modes require trusted code. Governed mode is not a filesystem/network
sandbox. It requires a supported POSIX runtime host. Unsupported requested
controls, such as network denial, are refused during generation; unavailable
runtime facilities or invalid artifacts fail startup. No Apizr installation is
needed to run either bundle. Install its own requirements.txt and use the same
startup commands below.
Governed startup checks policy, plan, source and artifact digests without importing
source. Changed source/plans/policy or missing worker files after startup cause
sanitized call errors; the server remains usable. Generation stays static in both
modes. OpenAPI/Tool definitions stay identical. The CLI reports execution.mode;
only governed bundles add an execution/ bridge and apizr_governed/ runtime.
Governed Tool errors are Invalid tool arguments, Tool execution timed out, or
Tool execution failed. No worker status, stderr, traceback or private path reaches
the client. Both stdio and Streamable HTTP support governed execution, using the
same SDK v2 / MCP 2026-07-28 contract and finite JSON result rules as direct MCP.
See governed transport architecture and the execution policy guide for defaults, limits, environment allowlists and the precise trust boundary.
Inspect the generated files
mcp-tools.json records the Tool names, input schemas, descriptions and original
capability IDs without starting a server. Simple ASCII function names are preserved;
long, Unicode or reserved names receive a deterministic name documented in the
architecture reference.
apizr-mcp.json records apizr.mcp/v1, protocol assumptions, source/IR/readiness
digests and artifact hashes. Source integrity is checked before runtime import,
and callable shape is checked afterwards. A modified source fails startup; rerun
generation from the intended source instead of editing its expected digest.
Only Tools are generated. Resources, Prompts, Tasks, Apps, Skills, agents,
repository scanning, containers, sandboxing and attestation are outside this path.
Historical generation and apizr generate rest remain available separately.
See the MCP architecture contract and REST guide for the shared validation semantics.
Governed OCI-container mode
There are three explicit modes: direct (default), governed local-process (v1 policy), and governed OCI-container (v2 policy). Local-process bundles and their bytes remain unchanged and do not require Docker.
Prepare a trusted local worker image as described in the execution guide, then use its full immutable ID and platform:
apizr generate mcp sample.py --output-dir ./generated-oci \
--execution-policy examples/policies/container-default.json \
--runtime-image sha256:<full-64-character-local-image-ID> \
--runtime-platform linux/amd64
Generation works without Docker and never pulls images. Both image and platform
are mandatory for OCI; image options with a local v1 policy are errors. Install
generated-oci/requirements.txt in the server environment; Apizr itself is not
required there. Docker CLI/Engine and the selected prepared worker image must be
available when starting the generated server. Startup checks fail before serving
if the provider or image is unavailable.
The interface definition is identical across modes; execution changes. Every OCI call uses a fresh container, so globals and mutable defaults reset. The transport never imports user source. The capability receives the existing OCI filesystem, network and resource limits. Subprocesses remain permitted but contained. See the v2 bridge contract for the control matrix, sanitized error mappings, integrity checks and trust boundary.