Skip to content

Make your first MCP and REST calls

Working with a training notebook? Follow the Data Scientist quickstart for Apizr 0.4.5: inspect, record two Runs, compare and serve a prediction.

Inspect a training script first (0.4.5)

With Apizr 0.4.5 installed, you can start with experiment evidence before building a service. This example requires only Apizr's core:

cat > example.py <<'PY'
from sklearn.metrics import roc_auc_score

learning_rate = 0.05
roc_auc_score(labels, predictions)
PY
apizr experiment inspect example.py

Parameters shows a static candidate; Metrics shows a recognized call with its value unobserved. Undefined names and an absent sklearn installation are fine because inspection does not execute the file. See the experiment guide for data fingerprints, notebooks and all eight sections.

Run a trusted experiment (0.4.5)

inspect never executes your code. run executes trusted code in a fresh process with host filesystem, network and subprocess access. It is not a security sandbox. Use a complete workload with its dependencies already installed in the same environment as Apizr. This separate example needs only the standard library:

cat > local_run.py <<'PY'
from pathlib import Path
mean = sum([2, 4, 6]) / 3
Path("model.json").write_text('{"mean": 4.0}\n')
PY
apizr experiment inspect local_run.py
RUN=$(apizr experiment run local_run.py --metric mean=mean --output model=model.json --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["run_digest"])')
apizr experiment list
apizr experiment show "$RUN"

The Run records the actual mean binding and the output's byte identity. The local store contains evidence JSON, not a copy of the model. See running experiments for notebooks, inputs, timeouts, failed Runs and the difference between intent and observation.

Inspect → Run → Compare

“My ROC AUC changed. What else changed between these runs?” This core-only example records two trusted local runs, then compares their saved evidence.

cat > compare_train.py <<'PYTHON'
max_depth = 8
score = 0.91
PYTHON
apizr experiment inspect compare_train.py
A=$(apizr experiment run compare_train.py --metric roc_auc=score --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["run_digest"])')
cat > compare_train.py <<'PYTHON'
max_depth = 12
score = 0.92
PYTHON
B=$(apizr experiment run compare_train.py --metric roc_auc=score --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["run_digest"])')
apizr experiment list
apizr experiment diff "$A" "$B"
rm compare_train.py
apizr experiment diff "$A" "$B" --format json > comparison.json

A → B means evidence added in B or removed from A; numeric deltas are B - A. show reads one Run; diff compares two Runs from the same validated local store. Comparison still works after deleting the source. Unknown observations remain unknown. Apizr identifies recorded differences; it does not prove which one caused the metric change. Read the comparison guide for data, environment and output evidence.

After reviewing a successful Run, use experiment expose RUN --capability ID --interface rest|mcp --output-dir DIR with explicit source authority. Select resources with --artifact NAME and dependencies with --dependency DISTRIBUTION; the recorded bytes and versions bind the resulting bundle. Repository Readiness still decides eligibility. See Experiment → serving.

Make a service from functions

Turn two Python functions into a local service. You will calculate a price (12.5 × 2 = 25.0) and check stock (3 ≤ 10 gives true). Prepare the example once, then choose REST or MCP. No plugin, Docker or AI account is needed for REST or the Python MCP client.

Prepare an isolated workspace

Install Apizr with the pip instructions and keep core/ activated. You need Python 3.11–3.14, curl, a POSIX shell on macOS/Linux and internet access for the example and server dependencies. In the same terminal:

mkdir apizr-quickstart
cd apizr-quickstart
apizr --version
python3 -m venv runtime

apizr --version should report outerspace-apizr 0.4.5. The new runtime/ environment will hold the generated server's dependencies separately from Apizr.

Get the example and choose its public functions

Download the three files from the recorded shop example:

mkdir shop
curl --fail --location https://raw.githubusercontent.com/Alien6-Studio/outerspace-apizr/37259eaf0a231d747f1ae0eb2f7ce94b2a2b306f/examples/repository-shop/api.py --output shop/api.py
curl --fail --location https://raw.githubusercontent.com/Alien6-Studio/outerspace-apizr/37259eaf0a231d747f1ae0eb2f7ce94b2a2b306f/examples/repository-shop/inventory.py --output shop/inventory.py
curl --fail --location https://raw.githubusercontent.com/Alien6-Studio/outerspace-apizr/37259eaf0a231d747f1ae0eb2f7ce94b2a2b306f/examples/repository-shop/pricing.py --output shop/pricing.py
ls shop

You should have api.py, inventory.py and pricing.py in shop/. Create the configuration that selects api.quote and inventory.available and permits Apizr to analyze this directory:

cat > readiness-direct.json <<'JSON'
{"execution":{"modes":["direct"]}}
JSON
cat > exposure-direct.json <<'JSON'
{"selection":{"include":["python:api:quote","python:inventory:available"]},"interfaces":["rest","mcp"],"execution":{"allowed":["direct"]}}
JSON
python - <<'PYTHON'
import json
from pathlib import Path
root = str(Path("shop").resolve(strict=True))
policy = {
    "schema": "apizr.operator-policy/v1",
    "grants": [{
        "adapter": "repository",
        "operation": "analyze",
        "target": {"kind": "local", "root": root},
        "permissions": ["source.analyze"],
    }],
}
Path("operator.json").write_text(json.dumps(policy, indent=2) + "\n")
print("Authorized source:", root)
PYTHON

The three files sit beside shop/. Keep that directory at the same location; its analysis permission names this exact path.

What do these three configuration files do?
  • Readiness: describes the kind of interface Apizr can generate here.
  • Exposure: selects the public functions and allows direct execution.
  • Operator: permits source analysis of this directory. It grants neither execution nor publication.

Private helpers are included when needed, but do not become public functions. For another project, review its source, select its functions and create an operator grant for its own root.

Choose your interface

Use REST Use MCP

Both paths use the files prepared above; choose one. Generation reads the source without running it. The generated server uses direct mode: calls execute trusted Python with your user permissions. Use execution policies when you need fresh workers or execution limits.

Use REST

Generate the API and install its runtime dependencies:

apizr expose build rest shop --operator-policy operator.json --readiness-policy readiness-direct.json --policy exposure-direct.json --output-dir build/rest
runtime/bin/python -m pip install -r build/rest/requirements.txt

build/rest contains app.py, openapi.json and the supporting source. Start the server on a free local port:

runtime/bin/uvicorn app:app --app-dir build/rest --host 127.0.0.1 --port 8000

Keep this terminal open. In a second terminal, call the two endpoints:

curl --fail --silent --show-error http://127.0.0.1:8000/capabilities/api.quote --header 'Content-Type: application/json' --data '{"unit_price":12.5,"quantity":2}'
curl --fail --silent --show-error http://127.0.0.1:8000/capabilities/inventory.available --header 'Content-Type: application/json' --data '{"stock":10,"requested":3}'

The response bodies are 25.0 and true (curl does not append a newline). Open the interactive API docs to try other inputs. Stop the server with Ctrl+C when finished.

You can now continue with your own code, or try MCP in the original terminal. For a different example, the REST guide includes a video showing function selection, HTTP calls and input validation.

Use MCP

Generate the MCP bundle

Generate a server for the same two functions:

apizr expose build mcp shop --operator-policy operator.json --readiness-policy readiness-direct.json --policy exposure-direct.json --output-dir build/mcp

build/mcp contains server.py, mcp-tools.json, requirements.txt and the supporting source. The tools are named api.quote and inventory.available. Use a new output directory if you generate again.

This is your business-function server. The optional Apizr analysis MCP server is a separate tool for inspecting projects; you do not need it here.

Connect a client and make two calls

Install the server dependencies in the runtime environment:

runtime/bin/python -m pip install -r build/mcp/requirements.txt

Choose your usual MCP client or the Python client below.

Connect your usual MCP client

For Claude Desktop on macOS, follow the official local-server guide. In Settings → Developer → Edit Config, add this entry to your existing mcpServers object. Replace both /absolute/path/apizr-quickstart prefixes with the full directory path printed by pwd; keep your other servers.

{
  "mcpServers": {
    "repository-shop": {
      "command": "/absolute/path/apizr-quickstart/runtime/bin/python",
      "args": [
        "/absolute/path/apizr-quickstart/build/mcp/server.py",
        "--transport",
        "stdio"
      ]
    }
  }
}

Restart the client. Ask it to call api.quote with unit_price=12.5, quantity=2, then inventory.available with stock=10, requested=3. Approve those calls and look at the tool results: {"result": 25.0} and {"result": true}. Other MCP clients can use the same command and arguments in their configuration.

Use Python — no AI account required

Run this from apizr-quickstart. It starts the server, discovers the tools and calls both of them using the installed MCP SDK:

runtime/bin/python - <<'PY'
import asyncio
import json
import sys
from pathlib import Path

from mcp import Client, StdioServerParameters


async def main():
    server = StdioServerParameters(
        command=sys.executable,
        args=[str(Path("build/mcp/server.py").resolve()), "--transport", "stdio"],
    )
    async with Client(server, read_timeout_seconds=10) as client:
        names = sorted(tool.name for tool in (await client.list_tools()).tools)
        assert names == ["api.quote", "inventory.available"], names
        print("tools:", ", ".join(names))
        quote = await client.call_tool("api.quote", {"unit_price": 12.5, "quantity": 2})
        available = await client.call_tool("inventory.available", {"stock": 10, "requested": 3})
        assert not quote.is_error and not available.is_error
        assert quote.structured_content == {"result": 25.0}
        assert available.structured_content == {"result": True}
        print("quote:", json.dumps(quote.structured_content))
        print("available:", json.dumps(available.structured_content))


asyncio.run(main())
PY

Expected output:

tools: api.quote, inventory.available
quote: {"result": 25.0}
available: {"result": true}

The client closes the connection when finished. The server may also write logs to stderr. See the official Python client documentation for more client options.

Starting server.py --transport stdio alone waits for an MCP client; it is not a chat terminal. The MCP guide includes a video of discovery, calls and error handling with the Python client.

Continue with your own code

Initialize your project, inspect its functions and select the ones to expose. Review its source and dependencies before running the generated service. Follow The full journey to understand each stage, or the exposure guide for repository configuration.

For Apizr 0.4.5, your selected function can use statically resolvable private helpers in other modules without exposing those helpers.