Skip to main content

An opinionated FastMCP runtime for authenticated MCP and REST services through Finch

Project description

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:

python -m pip install 'aviary-mcp==0.1.0rc5'
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:

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:

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:

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:

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:

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:

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:

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/. 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 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. 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aviary_mcp-0.1.0rc5.tar.gz (155.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aviary_mcp-0.1.0rc5-py3-none-any.whl (54.7 kB view details)

Uploaded Python 3

File details

Details for the file aviary_mcp-0.1.0rc5.tar.gz.

File metadata

  • Download URL: aviary_mcp-0.1.0rc5.tar.gz
  • Upload date:
  • Size: 155.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for aviary_mcp-0.1.0rc5.tar.gz
Algorithm Hash digest
SHA256 b585a9106d34a3c779e36e562acf3320c56d36c1b30c9f474b55199e603360f7
MD5 0bf60b5fe1957cb8f54f2e84d6960d1d
BLAKE2b-256 a4cd3e14694f3f6106f9a53576dedcd6ebd51e0ae1bdee96530363bac7dfed97

See more details on using hashes here.

Provenance

The following attestation bundles were made for aviary_mcp-0.1.0rc5.tar.gz:

Publisher: release.yml on DigiBugCat/aviary-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aviary_mcp-0.1.0rc5-py3-none-any.whl.

File metadata

  • Download URL: aviary_mcp-0.1.0rc5-py3-none-any.whl
  • Upload date:
  • Size: 54.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for aviary_mcp-0.1.0rc5-py3-none-any.whl
Algorithm Hash digest
SHA256 1c579865989643d56d9a11efd60d15e7bdcd28218d95337cbe11e7747e113a6a
MD5 c531118eac53b3178f44d7eb6cee5e82
BLAKE2b-256 e2a91385ab31450c77d75c4328c9c9d03dc65727b7e5f183134f8be83b5e660a

See more details on using hashes here.

Provenance

The following attestation bundles were made for aviary_mcp-0.1.0rc5-py3-none-any.whl:

Publisher: release.yml on DigiBugCat/aviary-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page