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.

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. Most applications use it through app.run(expose="finch"); create_finch_exposure() is public for async supervisors that need explicit lifecycle control.

For a Finch-native application, the runtime now owns that lifecycle directly:

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.0rc1.tar.gz (145.9 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.0rc1-py3-none-any.whl (47.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: aviary_mcp-0.1.0rc1.tar.gz
  • Upload date:
  • Size: 145.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for aviary_mcp-0.1.0rc1.tar.gz
Algorithm Hash digest
SHA256 efe0e2bf0f5ad29412e9c23c1822e96aa8c29b4fa2e4389441344f460fac324d
MD5 d9cdc62037d61c4e97050a32da79da08
BLAKE2b-256 f148e48400ec69b5aac06a2071d389fe443957c8fd87e160981928126cba9eaa

See more details on using hashes here.

File details

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

File metadata

  • Download URL: aviary_mcp-0.1.0rc1-py3-none-any.whl
  • Upload date:
  • Size: 47.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.12

File hashes

Hashes for aviary_mcp-0.1.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 96a73fab334f17e7af726d456ddcbfb7a74ef56d44d07de0abcc1f3d9efd2c8b
MD5 078fc883d42c6cefe52e2edd839adc58
BLAKE2b-256 04f1d693be8043f7d40b5d130a3d7f9d107ab9aebec52f853eda8e29fe3c3566

See more details on using hashes here.

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