Skip to main content

hayate-mcp

Hayate ecosystem: Start here · Production golden app · Tested compatibility

Mount an MCP server into a hayate app: an official MCP Python SDK bridge for ASGI and a focused, Pydantic-free tools runtime for Cloudflare Python Workers, both over the same Streamable HTTP boundary.

Status: alpha (0.x). Tracks the latest stable revision — MCP 2026-07-28 and 2025-11-25 on both CPython/ASGI and Cloudflare Python Workers. Modern clients use handshake-free server/discover, mandatory request metadata and routing headers, cache hints, resultType, MRTR, and stateless POSTs. Existing clients retain the 2025 initialize/session lifecycle. The internal design memo (Japanese) lives in DESIGN.md; release history is in CHANGELOG.md, and the 0.12 migration notes are in docs/migration-0.12.md.

import mcp.types as types
from hayate import Hayate
from mcp.server.lowlevel import Server
from hayate_mcp import McpMount


async def list_tools(_ctx, _params):
    return types.ListToolsResult(
        tools=[
            types.Tool(
                name="echo",
                input_schema={
                    "type": "object",
                    "properties": {"text": {"type": "string"}},
                    "required": ["text"],
                },
            )
        ]
    )


async def call_tool(_ctx, params):
    text = (params.arguments or {})["text"]
    return types.CallToolResult(content=[types.TextContent(type="text", text=text)])


server = Server(
    "my-tools",
    version="1.0.0",
    on_list_tools=list_tools,
    on_call_tool=call_tool,
)
app = Hayate()
McpMount(server, path="/mcp").register(app)

Serve it with any ASGI server (uvicorn server:app), then connect:

npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --transport http --method tools/list
claude mcp add my-tools --transport http http://127.0.0.1:8000/mcp

What it implements

Era ASGI Workers
MCP 2026-07-28 Handshake-free, stateless POST; JSON, response SSE, and live subscriptions/listen Handshake-free, stateless POST; tools-only JSON
MCP 2025-11-25 Stateful initialize/session lifecycle; JSON/SSE POST, optional GET SSE, DELETE Stateless compatibility lifecycle and tools

The modern path is always stateless, even when the same McpMount also serves legacy stateful clients. Every request carries the protocol revision and client capabilities in _meta, and the transport cross-checks MCP-Protocol-Version, Mcp-Method, Mcp-Name, and schema-declared Mcp-Param-* headers before dispatch. Removed lifecycle methods return JSON-RPC -32601 with HTTP 404. Header, capability, and protocol-version errors use the new -32020, -32021, and -32022 codes with HTTP 400.

ASGI delegates discovery, capabilities, method dispatch, cache hints, and MRTR result models to the official MCP Python SDK 2.x. Intermediate notifications such as progress are returned before the final result on a bounded SSE response stream. When an SDK server registers subscriptions/listen, Hayate serves it as a response-owned live SSE stream with acknowledgment-first ordering, subscription IDs, keepalives, proxy-buffer suppression, and deterministic cancellation when the response body closes. The Workers runtime implements the smaller tools capability it advertises and deliberately omits resources, prompts, logging, sampling, Tasks, and subscription streams.

CI runs the pinned official MCP conformance runner against a comprehensive SDK-backed fixture through this mount: 70/70 scenarios — 30 for 2025-11-25 and all 40 current 2026-07-28 core scenarios. That includes stateless discovery, result envelopes, caching, standard and custom headers, JSON Schema 2020-12, HTTP error mapping, capability negotiation, and all MRTR flows including multi-round encrypted requestState and tamper rejection. The upstream legacy multiple-POST fixture still sends a non-negotiated version (upstream #412); equivalent negotiated-version concurrency remains a local gate. Workers are verified separately in current workerd and through the official SDK 2.x client.

Browser requests are accepted automatically only when the endpoint and Origin are both loopback (localhost, 127.0.0.1, or ::1). For every other browser origin, pass the exact allowed origins with trusted_origins=[...]. Non-browser clients such as the official SDK, Inspector, and Claude Code do not send Origin and continue to work without configuration. The request's reflected Host value is never treated as an allow-list entry.

For 2025 clients, each stateful ASGI session stays pinned to the version returned by initialize; a different later header returns 400. For 2026 clients, there is no initialize or session ID: the request metadata and HTTP headers are the complete routing contract on every POST.

Modern capabilities and MRTR

Declare client capabilities a tool actually needs. Hayate rejects the call before the handler runs when the modern client did not opt in:

McpMount(
    server,
    tool_capabilities={
        "draft_with_model": {"sampling": {}},
    },
).register(app)

The same contract is available on Workers as @server.tool(..., required_capabilities={"sampling": {}}). Missing capabilities return -32021 with the structured requiredCapabilities payload required by MCP.

SDK-backed servers can return InputRequiredResult from tools/call, prompts/get, or resources/read. For low-level Server applications that carry requestState, install the SDK's security boundary so clients receive an authenticated, expiring token rather than application plaintext:

from mcp.server.request_state import RequestStateBoundary, RequestStateSecurity

server.middleware.append(
    RequestStateBoundary(
        RequestStateSecurity(keys=[request_state_key]),
        default_audience=server.name,
    )
)

Use a shared secret from your deployment's secret store when requests can land on multiple processes. Key rotation is supported by passing the active key first and older verification keys after it. The complete multi-round fixture is in examples/conformance/server.py.

Authorization (OAuth 2.0 Resource Server)

Pass an Authorization to require Bearer tokens and serve RFC 9728 Protected Resource Metadata (MCP Authorization, 2026-07-28 and 2025-11-25):

from hayate_mcp import Authorization, McpMount

McpMount(
    server,
    authorization=Authorization(
        resource="https://mcp.example.com/mcp",
        authorization_servers=["https://auth.example.com"],
        verify_token=verify,  # async (token) -> claims | None
        scopes_supported=["mcp", "documents:read"],
        required_scopes=["mcp"],
    ),
    tool_scopes={
        "read_document": ["documents:read"],
    },
).register(app)

Unauthenticated requests get 401 with WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource", so clients (Claude, Inspector) discover the authorization server. Token issuance is the AS's job — point verify_token at hayate-auth or any OAuth 2.1 authorization server. The verifier must check signature, issuer, expiry, and that the token audience/resource is this MCP server before it returns claims; Hayate treats returned claims as that verification decision.

Verified claims are normalized (subject, client_id, scopes) and are available inside tool handlers:

from hayate_mcp import get_principal


@server.call_tool()
async def call_tool(name, arguments):
    principal = get_principal()
    assert principal is not None
    # principal["subject"], principal["scopes"], ...

Insufficient global or per-tool scopes return 403 with a WWW-Authenticate step-up challenge. Legacy stateful sessions are bound to the creating (issuer, client_id, subject) identity; modern requests are authenticated independently on every stateless POST.

RFC 9449 DPoP is available without changing the stable Bearer default. A sender-constrained verifier needs the complete request, not just the opaque token:

from hayate_mcp import Authorization

authorization = Authorization(
    resource="https://folio.example/mcp",
    authorization_servers=["https://auth.example"],
    verify_request=dpop_request_verifier,  # async (Request) -> claims | None
    authorization_scheme="DPoP",
    scopes_supported=["mcp"],
    required_scopes=["mcp"],
)

Both McpMount and WorkerMcpMount pass the immutable Fetch Request to the verifier, allowing it to validate the proof signature, htm, htu, ath, token cnf.jkt, and replay state. hayate-auth provides a compatible DPoPRequestVerifier. Current official MCP SDK OAuth clients model Bearer tokens only, so DPoP remains an explicit client/server extension until SDK support lands.

Hayate request context

Tools mounted with register(app) can reuse request-scoped Hayate state, headers, and runtime bindings without adding them to model-visible arguments:

from hayate_mcp import get_request_context

context = get_request_context()
assert context is not None
database = context.env.DB
request_id = context.get("request_id")

The context is isolated across concurrent requests and reset when each request finishes. get_request_context() returns None outside a registered mount; the lower-level mount.fetch(request) API deliberately has no app context.

On Cloudflare Workers

Use WorkerMcpServer and WorkerMcpMount on a plain Worker — no Durable Object, Pydantic, or old SDK line is needed:

from hayate import Hayate
from hayate.adapters.workers import to_workers
from hayate_mcp import WorkerMcpMount, WorkerMcpServer

app = Hayate()
server = WorkerMcpServer(
    "my-tools",
    version="1.0.0",
    description="Edge-native Python tools.",
    website_url="https://example.com",
    tools_ttl_ms=60_000,
    cache_scope="public",
)


@server.tool(
    name="echo",
    description="Echo text.",
    input_schema={
        "type": "object",
        "properties": {"text": {"type": "string"}},
        "required": ["text"],
        "additionalProperties": False,
    },
)
async def echo(arguments):
    return f"echo: {arguments['text']}"


WorkerMcpMount(server).register(app)
Default = to_workers(app)

Tool input and structured output use JSON Schema 2020-12 and are validated inside request scope, keeping workerd global initialization entropy-safe. Modern discovery, required resultType, server identity metadata, cache hints, routing headers, arbitrary JSON structuredContent, and optional extension declarations use the same 2026 wire contract as ASGI. Correctable validation failures and ToolError become model-visible isError results, matching the official SDK. WorkerProtocolError preserves deliberate JSON-RPC codes, HTTP statuses, and headers for request-aware edge authentication or throttling; the modern protocol's mandatory HTTP mapping takes precedence for reserved error codes. Unexpected exceptions are logged and sanitized before reaching the model. OAuth and per-tool scopes use the same Authorization and get_principal() APIs as the SDK-backed mount.

See examples/workers. The Workers surface is stateless and does not advertise server-initiated streams or session state. Use the default SDK-backed ASGI mount (examples/echo) when you need those. CI builds the local wheel in an isolated project, boots current workerd, and connects with the official MCP SDK client.

Why

  • Python is MCP's largest ecosystem, yet mounting an MCP endpoint inside your own web app still goes through ASGI plumbing with known friction.
  • Cloudflare's remote-MCP story (Agents SDK, McpAgent) is TypeScript-first. hayate-mcp supplies a Python Workers path that speaks both current and compatibility MCP eras without requiring the Pydantic-based SDK in the edge bundle.

License

MIT

Download files

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

Source Distribution

hayate_mcp-0.12.0.tar.gz (35.0 kB view details)

Uploaded Source

Built Distribution

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

hayate_mcp-0.12.0-py3-none-any.whl (39.8 kB view details)

Uploaded Python 3

File details

Details for the file hayate_mcp-0.12.0.tar.gz.

File metadata

  • Download URL: hayate_mcp-0.12.0.tar.gz
  • Upload date:
  • Size: 35.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for hayate_mcp-0.12.0.tar.gz
Algorithm Hash digest
SHA256 10ae35260b0379a26af08fdbe3e342538cab7eeaf75cf5e149a5a63eb0966465
MD5 b81f21a5579bcc7c08a960afccb21b68
BLAKE2b-256 88cc33873b7f54d5da36d0073b51273d51b0626fce78f556d1cbe90a24fdd982

See more details on using hashes here.

Provenance

The following attestation bundles were made for hayate_mcp-0.12.0.tar.gz:

Publisher: release.yml on hayatepy/hayate-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 hayate_mcp-0.12.0-py3-none-any.whl.

File metadata

  • Download URL: hayate_mcp-0.12.0-py3-none-any.whl
  • Upload date:
  • Size: 39.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for hayate_mcp-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8ba09db04d4bb54e93d693ae99ff88f093f9b5e04350db30e82e49601877276a
MD5 cf9b834239bdaa88d84f944ab8b02dcb
BLAKE2b-256 d126f15a385a77350213c5a66cd16f36395bf8ac9178f320e04415bc72deb43f

See more details on using hashes here.

Provenance

The following attestation bundles were made for hayate_mcp-0.12.0-py3-none-any.whl:

Publisher: release.yml on hayatepy/hayate-mcp

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

Release history Release notifications | RSS feed

0.12.1

2 files

This release

0.12.0 This release

2 files

0.11.1

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

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