Metadata-Version: 2.4
Name: aviary-mcp
Version: 0.1.0rc5
Summary: An opinionated FastMCP runtime for authenticated MCP and REST services through Finch
License: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: anyio<5,>=4.5
Requires-Dist: fastmcp==3.4.4
Requires-Dist: httpx<1,>=0.28
Requires-Dist: pyjwt[crypto]<3,>=2.10
Provides-Extra: test
Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# AviaryMCP

AviaryMCP is a release-candidate, opinionated FastMCP runtime for publishing one
tool definition through MCP and a generated REST/OpenAPI interface. It extends
FastMCP through public APIs rather than maintaining a source fork, so MCP and
HTTP share the same registry, validation, middleware, handler, and authorization
decision.

Install the public release candidate from PyPI:

```console
python -m pip install 'aviary-mcp==0.1.0rc5'
```

```python
from aviary_mcp import AviaryMCP

app = AviaryMCP("calculator")

@app.tool
def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

app.run(transport="http", host="127.0.0.1", port=8000)
```

`access="local"` is the safe default: HTTP may bind only to loopback, and
in-process/stdio calls receive a local principal. An unauthenticated network
service requires explicit `access="public"`. Supplying `auth=` (or a FastMCP
3.4 token verifier through `mcp_auth=`) selects private mode.

An authenticated REST face uses normalized principals and operation-centric
scopes:

```python
from aviary_mcp import AviaryMCP, Principal, StaticKeyAuth

app = AviaryMCP(
    "calculator",
    auth=StaticKeyAuth({
        # Use a generated, high-entropy secret in production. The provider
        # immediately retains only its SHA-256 digest.
        "development-secret": Principal(
            subject="example-client",
            scopes={"tool:call:add"},
        ),
    }),
)
```

`LocalAuth`, `StaticKeyAuth`, `FinchAssertionAuth`, `AnyOf`, and `AllOf`
normalize callers into a `Principal`. One outer ASGI authentication boundary
protects `/mcp`, generated REST routes, and application custom routes together.
The generated REST operation calls
`invoke_tool`, which checks `tool:call:<tool-name>` (plus any scopes registered
with `require_scopes`) before entering FastMCP's normal validation, middleware,
and handler path. When application auth is configured, the generated catalog
and OpenAPI routes also require authentication so they do not disclose a
private service's capability schema. Custom transports can obtain identical
policy behavior by supplying their normalized principal to `invoke_tool`.

The HTTP server exposes:

- `POST /mcp` — FastMCP's streamable HTTP MCP transport
- `GET /api/v1/tools` — tool catalog
- `POST /api/v1/tools/{tool_name}` — JSON object arguments and a FastMCP
  `ToolResult` response
- `GET /api/v1/openapi.json` — generated OpenAPI 3.1 document
- `GET /birdz` — unauthenticated application liveness

Every non-health HTTP body, including `/mcp`, is bounded to 1 MiB by default
(`max_json_request_bytes=`). Generated errors use a stable JSON envelope, and
private OpenAPI documents publish their configured security schemes.

Finch edge identity uses an asymmetric, short-lived caller assertion:

```python
from aviary_mcp import AviaryMCP, FinchAssertionAuth

app = AviaryMCP(
    "calculator",
    auth=FinchAssertionAuth(tenant="andrew", service="calculator"),
)
```

The verifier fetches the rotating ES256 public JWKS from
`https://jwks.finchmcp.com/.well-known/finch-jwks.json` and binds each request
to its audience, method, exact local path and query, raw body digest, expiry,
and one-time assertion ID. Multi-process deployments must inject a shared
atomic replay store; the default replay store is process-local. Finch exposure
rejects local-trust auth trees and can warm the JWKS before registering ready.

The Finch control client can hold a dynamic registration lease against the
local Go agent contract:

```python
from aviary_mcp import FinchClient, Registration

client = FinchClient()
async with client.maintain(Registration(
    "calculator",
    "http://127.0.0.1:8000",
    routes=("/mcp", "/api/v1"),
)):
    ...
```

The Finch exposure controller supervises the HTTP server, dynamic lease,
first-run approval, readiness, and shutdown together. New applications choose
one explicit connector and then call ordinary `app.run()`.

`Finch.local` makes the application own a dedicated, zero-config Finch child:

```python
from aviary_mcp import AviaryMCP, Finch

app = AviaryMCP(
    "calculator",
    finch=Finch.local(
        path="calculator",
        binary="/usr/local/bin/finch",
    ),
)

@app.tool
def add(a: int, b: int) -> int:
    return a + b

app.run()
```

The absolute binary path is deliberately explicit: AviaryMCP does not download
or silently trust a `finch` found on ambient `PATH`. It requires Finch 1.6.0 or
newer and validates the stable `finch version --json` contract before launch.
The child runs only `finch aviary serve`, which ignores every `finch.yml`, skips
CLI/admin state, and disables hub-pushed binary updates. A non-secret project ID
and lock live under `.aviary/finch`; service refresh credentials live in private
per-user state keyed by that project ID, outside Git and Docker build contexts.

`Finch.agent` connects the application to a separately supervised agent over an
explicit Unix socket:

```python
app = AviaryMCP(
    "calculator",
    finch=Finch.agent(
        path="calculator",
        socket="/Users/me/.finch/run/control.sock",
    ),
)
app.run()
```

Both modes expose the same application and exact route allowlist: `/mcp`,
`/api/v1`, and `/birdz`. Tool decorators need no Finch-specific route code.
Private key access is the default and automatically installs the Finch caller
assertion verifier. Use `edge_auth="public"` only for a deliberately public app.
Custom staging/self-hosted hubs must supply matching `hub`, `issuer`, and
`jwks_url` roots; production roots are fixed.

The legacy `app.run(expose="finch", ...)` surface remains supported for existing
deployments, and `create_finch_exposure()` remains public for async supervisors
that need explicit lifecycle control.

The equivalent legacy form is:

```python
from aviary_mcp import AviaryMCP, FinchAssertionAuth

app = AviaryMCP(
    "calculator",
    access="private",
    auth=FinchAssertionAuth(tenant="andrew", service="calculator"),
)

app.run(
    expose="finch",
    app_path="calculator",
    edge_auth="key",          # private default; public is a separate approval
    enrollment_output="auto", # TTY instructions or one-line container JSON
)
```

The app registers `/mcp`, its configured `api_base`, and `/birdz` as one exact
allowlist. On first run, device approval is the default: the local Finch agent
generates the proof key and returns only a verification URL, user code, and
machine fingerprint. The SDK prints that safe prompt, waits while the operator
approves the exact service, routes, edge mode, and fingerprint, and resumes when
Finch has atomically saved a service-scoped credential. AviaryMCP never receives
a device secret, CLI/admin token, join ticket, or refresh credential.

`edge_auth="key"` is private by default. It requires an AviaryMCP app with
`FinchAssertionAuth`, and the assertion service must match `app_path`.
`edge_auth="public"` requires an explicitly `access="public"`, unauthenticated
app plus a separate public-edge confirmation in the browser; the two modes
cannot be mixed. `/birdz` is process liveness, while `/birdz/ready` returns 200
only after Finch confirms a live relay.

Try it:

```console
python -m venv .venv
.venv/bin/pip install -e '.[test]'
.venv/bin/python examples/calculator.py

curl http://127.0.0.1:8000/api/v1/tools
curl -X POST http://127.0.0.1:8000/api/v1/tools/add \
  -H 'content-type: application/json' -d '{"a": 20, "b": 22}'
```

## Production containers

The repository includes a multi-stage, non-root reference `Dockerfile` and a
sidecar Compose deployment under [`examples/docker-compose/`](examples/docker-compose/).
The default first run is credentialless: start the stack, read the one-line safe
enrollment event from the app logs, open `verification_uri_complete`, and approve
the displayed manifest. Finch retains the resulting scoped credential in its
private persistent state volume, and AviaryMCP pins the approved tenant into its
assertion verifier before becoming ready. `FINCH_TENANT` is an optional stricter
account pin, not required configuration. The application receives only a
dedicated-group Unix control socket and never receives Finch credentials.
Dependency artifacts are pinned in `uv.lock`; release builds can also gate the
complete lock with `scripts/dependency-lock-sha256.sh`.

Container liveness and readiness are intentionally separate. `/birdz` shows
that the app process is serving; `/birdz/ready` stays unavailable through device
approval and relay startup, then becomes healthy only when the Finch relay is
live. Orchestrators should gate traffic and rollout completion on readiness.

See [`docs/production.md`](docs/production.md) for release gates, container
security boundaries, deployment/rollback steps, and the remaining cloud and
operator decisions.

## Release-candidate boundaries

The runtime deliberately uses FastMCP 3.x public APIs and does not vendor or
patch FastMCP. FastMCP child servers can be composed with the public live
`mount()` API; see [`docs/composition.md`](docs/composition.md). REST
streaming/background tasks and idempotency remain future work. The generated HTTP operation invokes
`FastMCP.call_tool`, so FastMCP middleware and input validation are shared rather
than reimplemented.

### Current MCP authentication boundary

FastMCP token verifiers passed as `mcp_auth=` are adapted through their public
`verify_token()` API and participate in the same outer request boundary as
Aviary providers. This supports direct bearer JWT/OAuth access without a split
REST/MCP configuration. The adapter does not mount a FastMCP OAuth provider's
authorization-server or discovery routes; Finch owns OAuth at the edge for this
release candidate. Hosted standalone OAuth discovery remains a post-pilot item.

`FinchAssertionAuth` includes a bounded in-process replay cache by default. A
multi-process or horizontally scaled application must provide an atomic shared
replay store before production traffic so a one-time assertion cannot be
accepted by two workers.
