Skip to content

Apizr as a local MCP server

0.4 development — not released

This feature requires wheels built from this development source, not the published 0.3.0 package. Follow the development installation and record the source commit. This page ships with the feature PR; it does not describe a released plugin. The public site's build marker identifies which documentation revision has actually been deployed.

The optional apizr-mcp plugin exposes read-only local analysis and planning through stdio. It does not generate bundles, execute business functions, acquire Git sources, install plugins or publish artifacts. A generated business MCP server exposes the capabilities you selected; this server exposes Apizr's compiler operations. They are different applications.

Install and activate explicitly

Use macOS or Linux, Python 3.11–3.14 and uv. Build the core and plugin from the same recorded commit. Preparation below can download build tools and locked dependencies. Installation by Apizr uses only the reviewed local wheelhouse. The SDK and a copy of the compiler belong to the plugin's separate environment; the minimal core receives neither MCP nor REST dependencies.

From the source checkout:

work=$(mktemp -d)
git rev-parse HEAD > "$work/source-commit.txt"
uv build --wheel --out-dir "$work/wheels"
uv build --wheel plugins/mcp --out-dir "$work/wheels"
uv export --locked --no-dev --extra mcp --no-emit-project \
  --output-file "$work/dependencies.txt"
cp -R examples/project-config "$work/project"
cd "$work"
uv venv --seed --no-python-downloads --python python3 prepare
prepare/bin/python -m pip download --only-binary=:all: --dest wheels \
  -r dependencies.txt
uv venv --no-python-downloads --python python3 core
uv pip install --python core/bin/python --offline --no-index --find-links wheels \
  wheels/outerspace_apizr-0.3.0-py3-none-any.whl

Record one exact version and SHA-256 per distribution, including the plugin, core and all transitive dependencies. The export uses the repository's reviewed lock; the final lock describes the actual wheels for this Python/platform. Hashes establish integrity, not trust in their authors.

prepare/bin/python - <<'PY'
import hashlib
import zipfile
from email.parser import BytesParser
from pathlib import Path

lines = []
for wheel in sorted(Path("wheels").glob("*.whl")):
    with zipfile.ZipFile(wheel) as archive:
        name = next(n for n in archive.namelist() if n.endswith(".dist-info/METADATA"))
        metadata = BytesParser().parsebytes(archive.read(name))
    digest = hashlib.sha256(wheel.read_bytes()).hexdigest()
    lines.append(f"{metadata['Name']}=={metadata['Version']} --hash=sha256:{digest}\n")
Path("plugin.lock").write_text("".join(lines))
PY
plugin_sha=$(prepare/bin/python -c 'import hashlib; from pathlib import Path; print(hashlib.sha256(Path("wheels/apizr_mcp-0.0.0-py3-none-any.whl").read_bytes()).hexdigest())')
core/bin/apizr plugins install wheels/apizr_mcp-0.0.0-py3-none-any.whl \
  --sha256 "$plugin_sha" --requirements plugin.lock --wheelhouse wheels \
  --plugins-dir "$work/plugins"
core/bin/apizr plugins enable apizr-mcp --version 0.0.0 --plugins-dir "$work/plugins"

Installation alone leaves the plugin inactive. The existing locked installation rules apply. The plugin's development version remains 0.0.0; this is not a package published on PyPI. No installer runs at server startup. A missing, inactive, inconsistent or missing-interpreter installation is refused without repair.

Choose one local project

The copied project/ contains this complete apizr.toml:

schema_version = "apizr.project/v1"
root = "."
readiness_policy = "policies/readiness.json"
exposure_policy = "policies/exposure.json"

[scan]
source_roots = ["src"]
excluded_directories = ["ignored"]
max_file_bytes = 4096

[graph]
max_ast_nodes = 10000

src/calculator.py:

def add(a: int, b: int = 1) -> int:
    return a + b

policies/readiness.json:

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

policies/exposure.json:

{
  "selection": {"include": ["python:calculator:add"]},
  "interfaces": ["rest", "mcp"],
  "execution": {"allowed": ["direct"]}
}

Paths resolve relative to apizr.toml, not the client's working directory. The server freezes configuration and policy values at startup. Restart explicitly to apply operator edits. Policy files must be regular files, at most 64 KiB, without duplicate JSON keys. The existing project loader bounds TOML input. The selected root is anchored by filesystem identity; symlink traversal and root replacement cannot redirect a calculation to another project. Existing scanner refusals and diagnostics remain visible.

Start with absolute paths:

"$work/core/bin/apizr" mcp serve --project "$work/project/apizr.toml" \
  --plugins-dir "$work/plugins"

Without --plugins-dir, the existing default plugin store applies. The core validates the active installation and replaces itself with that environment's interpreter and the registered apizr_mcp module. It passes an empty environment, uses isolated Python, and reserves stdout for MCP. Diagnostics use stderr.

This durable connection is not apizr plugins run: that command retains its one-request apizr.extension/v1 protocol and existing timeout. Internally, the MCP server uses that runtime for fresh, isolated compiler calculations, not as an envelope around MCP traffic. Those workers call Python APIs, not the CLI.

Tools and results

Tool Arguments Structured result
apizr_analyze Optional expected_repository_digest Existing canonical catalog, graph, and repository_digest; no retained raw sources
apizr_readiness Optional expected_repository_digest Existing canonical report, repository_digest, and the report's exit_code
apizr_plan_exposure Optional policy (existing ExposurePolicy) and expected_repository_digest Existing canonical ExposurePlan, including repository_digest

For example, call apizr_analyze with {}, retain result.structured_content["repository_digest"]["value"], then call apizr_readiness with {"expected_repository_digest": "<that 64-digit digest>"}. Call apizr_plan_exposure with {} to use the startup exposure policy, or {"policy": {"selection": {"include": ["python:calculator:add"]}, "interfaces": ["mcp"], "execution": {"allowed": ["direct"]}}} to propose a plan. The proposal never overwrites files or authorizes execution.

A missing exposure policy yields policy_required; it never selects everything. A changed digest yields repository_changed. Each call scans once; separate calls can see changed source files. Readiness exit_code: 1 is a valid business report with isError: false, preserving unknown/conditional/refused distinctions.

Errors have isError: true and structured error.code/error.diagnostics: invalid_arguments, exposure_refused (canonical planning diagnostics), scope_changed, repository_changed, and redacted runtime/operational codes. No internal traceback or raw subprocess diagnostic is returned. Strict tool schemas reject client-supplied roots, policy paths, executables or startup limits. Tool descriptions and read-only annotations are fixed; repository text remains data and cannot create tools or replace server instructions. Annotations alone are not an authorization boundary.

Bounds and lifecycle

Startup-only options:

Option Default Accepted range
--timeout-ms 10,000 per calculation 1–600,000
--max-request-bytes 65,536 per stdio line 4,096–1,048,576
--max-response-bytes 4,194,304 2,048–16,777,216

The argument allowance reserves 2 KiB within the request bound. Worker stdout (including its private response envelope) is bounded while reading; an overflow returns size_limit, never a truncated canonical report. Result serialization is also checked (response_too_large). The SDK's protocol envelope and schemas have a separate 256 KiB allowance. An oversized/invalid byte stream closes the connection with a redacted stderr diagnostic such as request_too_large. Protocol writes have a five-second deadline. Scanner limits still apply; these bounds are not an operating-system memory quota or a sandbox.

One calculation runs at a time. Overlapping calls get server_busy; discovery remains responsive. Timeout affects a calculation, not the connection lifetime. MCP cancellation, EOF, client disconnection and SIGINT/SIGTERM/SIGHUP cancel active work through the existing runtime and wait for pipe closure and child recovery. Unconfirmed cleanup ends the session with an explicit diagnostic. Ordinary process-group descendants are covered by the runtime; detached descendants remain outside its documented cleanup guarantees. No plugin or project code is imported by the core, and project code is not executed by analysis.

There is no automatic .env load, inherited secret environment, HTTP/SSE server, OAuth, sampling, LLM call, telemetry, network acquisition or publication. Results may contain project names, docstrings and diagnostics: the MCP client may send them to its model. Local stdio alone does not guarantee confidentiality.

Client configuration and validation

The real integration proof uses the official Python MCP SDK 2.2.0, its current low-level API, and a real SDK stdio client. It validates revisions 2026-07-28 (auto) and 2025-11-25 (legacy). No JSON-RPC server implementation is duplicated here.

This VS Code .vscode/mcp.json example follows the official MCP configuration reference. Replace all paths; the graphical VS Code connection has not been tested. No personal client configuration is changed by installation or this proof.

{
  "servers": {
    "apizr": {
      "type": "stdio",
      "command": "/absolute/work/core/bin/apizr",
      "args": [
        "mcp", "serve", "--project", "/absolute/work/project/apizr.toml",
        "--plugins-dir", "/absolute/work/plugins"
      ]
    }
  }
}

From the checkout, uv run --locked python scripts/smoke_mcp_server.py --output /absolute/new-proof-directory builds and installs separate wheels outside the checkout, checks inactive refusal and explicit activation, compares all tools against Python/CLI, and exercises actual subprocess cancellation/shutdown with a synchronized test worker. It checks core files, activations and project contents. The dedicated CI matrix covers Linux 3.11–3.14 and macOS 3.11/3.14 and preserves logs and separate plugin coverage. Linux repeats the installed proof with network access removed by a disposable network namespace. These are development proofs, not tests of a graphical client or a claim that 0.4 has been published.