Skip to content

Plan and build REST/MCP from a repository

Repository analysis requires an explicit operator policy. Save a grant for the exact source root as operator.json; follow Authorize source analysis. This permission does not expose functions or authorize their execution.

For one Python file or notebook, use the REST or MCP single-source guide. This page covers repository-level selection and bundles.

Functions with supported TypedDict inputs use the same structured contracts in repository bundles. Keep each declaration and its annotated public function in the same module. Select the function's capability ID; its local helpers stay private. An unrelated unfinished module remains visible in the global audit while an independent selection can generate. The closed nested input shape also travels into local-process and OCI workers.

Readiness describes eligibility under a policy. Exposure records what the operator explicitly selects for publication, through which interfaces and under which execution requirements. It does not generate or start a server.

An exposure plan records an explicit publication decision over static evidence; it is not an authorization grant or runtime safety proof.

Three policies, three responsibilities

Input Responsibility Backend meaning
--readiness-policy Assess static evidence and execution-contract compatibility Which modes/controls may satisfy readiness
--policy (Exposure Policy) Select exact public capability IDs and interfaces Which compatible modes are permitted for this selection
--execution-policy Configure actual invocation controls v1 chooses local-process; v2 chooses OCI; omission chooses direct

Readiness and exposure constrain eligibility; execution configures the boundary. The chosen backend must appear in every selected record's compatible modes. A mismatch refuses the whole bundle: no ranking, widening or fallback. An Exposure Plan may permit several modes, but a generated server uses exactly one.

READY does not mean exposed. A helper/dependency stays private unless explicitly selected. Planning/building records a publication decision; it does not deploy or start the resulting service.

Committed small examples

From the source checkout, use examples/repository-shop and the matching files in examples/policies:

Mode Readiness Exposure Execution
Direct readiness-direct.json exposure-direct.json Omit
Local readiness-local.json exposure-local.json execution-local.json
OCI readiness-oci.json exposure-oci.json execution-oci.json
apizr expose build mcp examples/repository-shop --operator-policy operator.json --readiness-policy examples/policies/readiness-local.json --policy examples/policies/exposure-local.json --execution-policy examples/policies/execution-local.json --output-dir .output/shop-local

For OCI, choose the three oci files and add --runtime-image sha256:<64hex> and --runtime-platform linux/amd64 (or linux/arm64). Replace the placeholder with an existing repository-worker image ID. The policies contain no secret or image value. No policy stack is merged implicitly, and application dependencies are not installed.

Choose capabilities

For shop/pricing.py defining calculate, use the exact capability ID displayed by Scan/Readiness. Declare an interface and at least one allowed execution mode:

apizr expose plan . --operator-policy operator.json --interface mcp --execution-mode oci-container \
  --select python:shop.pricing:calculate

Repeat --select, --interface and --execution-mode for multiple choices. Multiple compatible modes remain in the plan; there is no preferred backend. --all-ready deliberately includes every repository READY capability. --exclude python:admin:delete_user always wins over includes. Unknown include or exclude IDs are errors, including typos in exclusions. Without any selector, a valid empty plan is returned with a warning in the human report.

Use policy files

Save exposure.json:

{
  "schema_version": "apizr.exposure-policy/v1",
  "selection": {
    "include": ["python:shop.pricing:calculate"],
    "include_all_ready": false,
    "exclude": []
  },
  "interfaces": ["rest", "mcp"],
  "eligibility": {"allow_conditional": false},
  "execution": {
    "allowed": ["local-process", "oci-container"],
    "require": ["wall_timeout"]
  }
}
apizr expose plan . --operator-policy operator.json --policy exposure.json
apizr expose plan . --operator-policy operator.json --policy exposure.json --plan > exposure-plan.json
apizr expose plan . --operator-policy operator.json --readiness-policy readiness.json --policy exposure.json

Policy files use JSON. --policy cannot be mixed with inline exposure choices; --readiness-policy remains independent. The default readiness policy is the existing deterministic OSS policy. Its execution modes are local-process and OCI; to plan direct execution, explicitly include direct in both policies:

{"execution": {"modes": ["direct"], "require_controls": []}}

That fragment is a readiness policy. The exposure policy uses "execution": {"allowed": ["direct"]}.

Inline flags also include --allow-conditional and repeated --require-control. Requiring network_deny narrows compatible modes to OCI; subprocess_deny is not advertised by the frozen v1 static backend adapters. The additional strict OCI profile is selected in the separate execution policy and verified against its worker image. Docker/image availability is not checked. No runtime image is part of the plan.

Read the result

The human report lists total/READY/selected/planned counts, interfaces, contract-compatible execution counts, selected IDs, retained uncertainty and observed support/external modules. --plan emits only canonical JSON on success. The artifact binds Catalog, Graph, Repository Readiness and Exposure Policy by digest, enabling deterministic review across filesystem locations.

  • READY selections must still satisfy interface and execution requirements.
  • CONDITIONAL requires explicit opt-in and an eligible shared interface contract. The opt-in cannot override unresolved local type/callable contracts.
  • AMBIGUOUS and UNSUPPORTED are always refused.
  • Missing required evidence or a repository-wide integrity failure is refused.
  • Any refused selection blocks the whole plan. No partial result is published.

If A calls B, selecting A does not expose B. The plan keeps the direct relationship as evidence. Supporting modules are observed direct requirements, not a complete packaging closure. Effects are copied, never newly inferred or propagated. External module names are lexical evidence without environment resolution or package classification.

Exit codes and bounds

Exit Meaning Output
0 Valid plan, including an empty plan Human report or canonical JSON on stdout
1 Selection/evidence/policy refusal Deterministic diagnostics on stderr; no plan bytes
2 Invalid policy, malformed ID, invalid/inaccessible repository input Sanitized error on stderr; no plan bytes

Argument-parser errors also exit 2. Bound discovery/graph failures that yield incomplete evidence exit 1. All existing scan/graph bounds are available: --source-root, --exclude-dir, file/total/entry/depth limits and AST, relationship, call and import limits. Each source is discovered/read once; planning does not rescan it.

Build a direct repository bundle

Create direct-readiness.json:

{"execution": {"modes": ["direct"]}}

Then build one interface for your explicitly selected eligible capabilities:

apizr expose build rest . --operator-policy operator.json --source-root src \
  --readiness-policy direct-readiness.json \
  --interface rest --execution-mode direct \
  --select python:shop.api:run --select python:shop.pricing:run \
  --output-dir .output/rest

apizr expose build mcp . --operator-policy operator.json --source-root src \
  --readiness-policy direct-readiness.json --policy direct-exposure.json \
  --output-dir .output/mcp

For the second example, create direct-exposure.json with interfaces: ["mcp"], execution: {"allowed": ["direct"]} and explicit selection IDs, using the Exposure Policy format above. The target must already be permitted by the policy. The builder cannot use the earlier OCI-only example or silently add direct mode.

REST routes and MCP Tool names include the module, such as shop.api.run and shop.pricing.run. Only selected capabilities are public. All inspected sources under the scanner roots are copied, including unselected helpers and modules; use --source-root and --exclude-dir to keep tests or other code out of scope. Unrelated bundled modules are not imported simply because they are present.

Run the generated artifacts in a prepared environment:

cd .output/rest
python -m pip install -r requirements.txt
uvicorn app:app --host 127.0.0.1 --port 8000

For MCP, use python server.py --transport stdio or python server.py --transport streamable-http --port 8000 from its output directory. Transport requirements do not install your application's dependencies; provisioning those is an explicit operator responsibility. Generation never queries or installs them. Source/resources outside the scanned Python universe are not copied.

These servers execute trusted code directly. Global state and mutable defaults persist across calls. Add an explicit execution policy for a fresh local worker or OCI container per invocation, as described below.

Use private helpers from the same repository

In Apizr 0.4.5, you can organize your Python code into modules and expose the function you choose when its local dependencies can be resolved statically. Helpers are packaged as implementation support and stay private unless you select them too. Published 0.4.4 retains the earlier import restriction.

For example, create this small project:

project/
  features.py
  predict.py
# features.py
def normalize(value: float) -> float:
    return value / 100
# predict.py
from features import normalize


def predict(value: float) -> float:
    return normalize(value) * 0.8

Save an analysis grant for the exact project root in operator.json, as explained at the top of this guide. Use the direct-readiness.json policy above:

apizr expose plan project --operator-policy operator.json \
  --readiness-policy direct-readiness.json \
  --interface rest --interface mcp --execution-mode direct \
  --select python:predict:predict

apizr expose build rest project --operator-policy operator.json \
  --readiness-policy direct-readiness.json \
  --interface rest --execution-mode direct --select python:predict:predict \
  --output-dir .output/predict-rest

apizr expose build mcp project --operator-policy operator.json \
  --readiness-policy direct-readiness.json \
  --interface mcp --execution-mode direct --select python:predict:predict \
  --output-dir .output/predict-mcp

Both bundles include features.py and predict.py. Only predict.predict is public: send {"value": 50.0} to /capabilities/predict.predict for REST, or call the MCP Tool predict.predict. REST returns 0.4; MCP returns {"result": 0.4}. Start the servers as described above, in an environment with their requirements.

The supported forms include from features import normalize, its stable alias, and import features followed by features.normalize(...). A proper package with an empty __init__.py can use a uniquely resolved relative import. Ambiguous, star, rebound, dynamic and unresolved external imports remain refused. Required helper bindings and module initialization must also have sufficient evidence. A nonempty package initializer without callable Readiness evidence remains conditional, even when it contains only a docstring. Private helpers do not need their own public JSON input contract. Their effects are not inferred or propagated.

Single-file generation remains conservative because it has no repository evidence. --allow-conditional does not waive unresolved interface contracts. Repository bundles still copy the complete inspected source universe. Apizr 0.4.5 release checks the selected function's required helpers and imports as described below. See Repository Readiness for the retained import evidence and its limits.

Work with unfinished research code

In Apizr 0.4.5, you can expose a serving function without first cleaning up every old experiment or notebook in your research repository. Apizr checks the function you select and the code it needs. A problem in a required feature helper or import still blocks generation.

The committed examples/research-serving project contains:

project/
├── serving.py       ← predict: selected
├── features.py      ← normalize: private helper
├── experiments.py   ← unfinished experiment
└── old_notebook.py  ← ambiguous import, unrelated to predict

Copy that directory to project. Save an analysis grant for its exact root in operator.json and use the direct-readiness.json policy above. With the published 0.4.5 package, run:

apizr expose plan project --operator-policy operator.json \
  --readiness-policy direct-readiness.json \
  --interface rest --interface mcp --execution-mode direct \
  --select python:serving:predict --plan > exposure-plan.json

apizr expose build rest project --operator-policy operator.json \
  --readiness-policy direct-readiness.json \
  --interface rest --execution-mode direct --select python:serving:predict \
  --output-dir .output/predict-rest

apizr expose build mcp project --operator-policy operator.json \
  --readiness-policy direct-readiness.json \
  --interface mcp --execution-mode direct --select python:serving:predict \
  --output-dir .output/predict-mcp

Only serving.predict is public. As in the example above, {"value": 50.0} returns 0.4 through REST and {"result": 0.4} through MCP. The full audit still reports the ambiguous import in old_notebook.py, and the inspected sources remain in both bundles. Unrelated files are not imported just because they are included.

If predict starts depending on that ambiguous import, Apizr refuses the request and shows the dependency path to the problem. Repository-wide limits or corrupt evidence also block generation. This helps you separate serving code from unfinished training experiments; it does not establish runtime safety. Published 0.4.4 keeps the earlier repository-wide completeness requirement.

Build exits 0 on success, 1 on exposure/bundle refusal, and 2 on invalid or operational input. A valid empty Exposure Plan is refused for a server build. The output must be new or empty: staging and atomic publication prevent partial bundles and overwriting existing contents. There is no saved-plan CLI shortcut.

See Repository Bundle v1 for exact integrity, packaging and runtime semantics, and Exposure Plan v1 for planning contracts.

Build a governed repository bundle

Use an exposure policy whose execution.allowed explicitly includes the requested backend for every selection. The readiness policy must also allow it. For local execution, create local-policy.json containing {} and use:

apizr expose build mcp . --operator-policy operator.json \
  --readiness-policy readiness.json --policy exposure.json \
  --execution-policy local-policy.json --output-dir .output/mcp

For example, the readiness execution configuration is {"execution":{"modes":["local-process"]}} and the exposure execution configuration is {"execution":{"allowed":["local-process"]}}. Keep your explicit selection and interfaces: ["mcp"] in the exposure policy.

For OCI, allow oci-container in those policies, create oci-policy.json containing {"schema_version":"apizr.execution/v2"}, and supply an existing image ID/platform:

apizr expose build mcp . --operator-policy operator.json \
  --readiness-policy readiness.json --policy exposure.json \
  --execution-policy oci-policy.json \
  --runtime-image sha256:<64hex> --runtime-platform linux/amd64 \
  --output-dir .output/mcp

Replace the image placeholder with the immutable image ID. It must advertise org.apizr.repository.worker.protocol=apizr.repository-runtime/v1 as well as the existing single-source worker label. No image is pulled automatically. Generation is static and works without Docker; the generated server checks availability at startup and invocation. CLI output describes the configured backend.

Use rest and a REST exposure policy for the corresponding HTTP server. Install the emitted requirements and start generated servers as in the direct examples. The transport never imports project source. Each call revalidates all artifacts and copies the exact Python source universe into a fresh worker directory. Globals, mutable defaults and support-module state reset on every governed call; direct servers keep their existing persistent-state behavior.

Local workers provide timeout/bounds/environment/process cleanup, not a filesystem or network sandbox. OCI adds the established container controls and requires the repository-aware image. Local execution cannot prohibit subprocesses. OCI offers an opt-in strict subprocess-deny profile, and OCI is not a VM. Missing backends refuse operation without fallback. Application libraries must be installed in the execution Python environment or selected image; no dependency installation/inference or resource-file packaging is added.

See Governed repository execution for contracts, tamper validation, error mappings and deployment responsibilities.

Client collections

Apizr can export the retained REST bundle to Postman, Bruno and Insomnia collections, with deterministic files and safe local regeneration. No source scan or execution is needed.