diff --git a/docs/HTTP_DEPLOYMENT.md b/docs/HTTP_DEPLOYMENT.md index a9d6357..38fd463 100644 --- a/docs/HTTP_DEPLOYMENT.md +++ b/docs/HTTP_DEPLOYMENT.md @@ -4,14 +4,7 @@ The Zernio MCP server can be deployed over HTTP, allowing remote access from any MCP client. Each user provides their own Zernio API key when connecting. -The server exposes **two transports simultaneously**: - -| Transport | Endpoint | When to use | -| --- | --- | --- | -| **Streamable HTTP** (recommended) | `POST /mcp` | Modern MCP transport. Survives proxies/bridges that drop idle connections. Use this for Claude Code, `mcp-remote`, and any new client. | -| **SSE** (legacy) | `GET /sse` + `POST /messages/` | Older two-endpoint transport. Kept for backwards compatibility. The long-idle `GET /sse` connection can be killed by load balancers or bridges after a few minutes idle. | - -Pick Streamable HTTP unless you have a specific client that only speaks SSE. +The server's primary transport is **Streamable HTTP** (`POST /mcp`), the modern MCP transport supported by all current clients. Each request/response is self-contained (with optional chunked streaming), so it survives proxies and bridges that drop idle connections. The legacy SSE transport (`GET /sse` + `POST /messages/`) is kept for backward compatibility. ## Quick Start @@ -32,7 +25,7 @@ uv run zernio-mcp-http # Health check (no auth needed) curl http://localhost:8080/health -# Server info (lists both transports, no auth needed) +# Server info (no auth needed) curl http://localhost:8080/ # Streamable HTTP endpoint (requires your Zernio API key) @@ -40,9 +33,6 @@ curl -H "Authorization: Bearer your_zernio_api_key" \ -H "Accept: application/json, text/event-stream" \ -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' - -# SSE endpoint (legacy) -curl -H "Authorization: Bearer your_zernio_api_key" http://localhost:8080/sse ``` ## Railway Deployment @@ -61,6 +51,8 @@ The server doesn't require any environment variables. Users authenticate by prov Optional variables: - `HOST` (default: 0.0.0.0) - `PORT` (default: 8080, Railway sets this automatically) +- `MCP_PUBLIC_URL` (default: https://mcp.zernio.com) — the server's canonical public URL, used as the OAuth resource identifier in the discovery metadata. Set it for non-prod deployments (e.g. a Railway preview URL). +- `MCP_ALLOWED_ORIGINS` — comma-separated hostnames appended to the browser Origin allowlist (DNS-rebinding protection). ## Connecting Clients @@ -84,17 +76,6 @@ Configuration in MCP settings: } ``` -### Claude Code CLI (legacy: SSE) - -If you need SSE for an older client, the endpoint is `/sse` instead of `/mcp`: - -```bash -claude mcp add --transport sse zernio https://your-app.railway.app/sse \ - --header "Authorization: Bearer your_zernio_api_key_here" -``` - -Note: SSE connections can be dropped by proxies after a few minutes idle. Prefer Streamable HTTP if your client supports it. - ### Python Client (Streamable HTTP) ```python @@ -112,23 +93,6 @@ async with streamablehttp_client( pass ``` -### Python Client (SSE, legacy) - -```python -from mcp.client.sse import sse_client - -headers = { - "Authorization": "Bearer your_zernio_api_key_here" -} - -async with sse_client( - "https://your-app.railway.app/sse", - headers=headers -) as (read, write): - # Use MCP client - pass -``` - ## Authentication Each user must provide their own Zernio API key when connecting using the standard HTTP Authorization header: @@ -147,13 +111,16 @@ curl -H "Authorization: Bearer sk_your_api_key_here" \ The server validates the API key by making a test request to the Zernio API. If valid, the request is processed and the API key is used for all operations within that request. +The server is an OAuth 2.0 **resource server** (FastMCP's resource-server model): both plain Zernio API keys and OAuth access tokens issued by zernio.com are accepted as the bearer. Clients without a token (e.g. Claude's connector) receive a `401` whose `WWW-Authenticate` challenge points at the RFC 9728 discovery document at `/.well-known/oauth-protected-resource/mcp`, from which they discover the zernio.com authorization server and run the OAuth flow. The pre-FastMCP discovery path (`/.well-known/oauth-protected-resource`) permanently redirects to the canonical document, so clients holding the old URL keep working. + ## Security -- Each user's API key is validated against the Zernio API -- API keys are stored per-request using Python's `contextvars` (Streamable HTTP runs in stateless mode, so keys never leak across requests) +- Each user's bearer (API key or OAuth token) is validated against the Zernio API on every request +- The validated bearer is scoped to its request via FastMCP's access-token context (Streamable HTTP runs in stateless mode, so credentials never leak across requests) - No shared credentials or server-wide API keys -- Health check endpoint is public (no auth required) -- All other endpoints require authentication +- Browser requests are checked against an Origin allowlist for DNS-rebinding protection (extend with `MCP_ALLOWED_ORIGINS`) +- Health check, server info, and OAuth discovery endpoints are public (no auth required) +- The MCP and legacy SSE endpoints require authentication ## Get Your Zernio API Key diff --git a/docs/MCP.md b/docs/MCP.md index c80fa10..8a88ce9 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -88,8 +88,10 @@ Since Claude can't access local files, use the browser upload flow: | `posts_retry` | Retry a failed post | | `media_generate_upload_link` | Get link to upload media | +These core commands are always visible. The full Zernio API catalog (~400 tools) is reachable on demand through tool search: `search_tools` finds a capability by description and `call_tool` invokes it — and every underlying tool also stays directly callable by name. + ## Remote Access (HTTP) -The remote MCP server exposes both **Streamable HTTP** (`POST /mcp`, recommended) and **SSE** (`GET /sse`, legacy) transports. Use Streamable HTTP unless your client only supports SSE — it survives idle timeouts on proxies and bridges like `mcp-remote`. +The remote MCP server speaks **Streamable HTTP** (`POST /mcp`) — the modern single-endpoint transport supported by all current MCP clients. It survives idle timeouts on proxies and bridges like `mcp-remote`. The legacy SSE transport (`GET /sse`) is kept for backward compatibility. For deployment and client setup, see the [HTTP Deployment Guide](HTTP_DEPLOYMENT.md). diff --git a/pyproject.toml b/pyproject.toml index 244eb85..dcdd179 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ all = [ "mcp>=1.8.0", ] mcp = [ + "fastmcp>=3.1.0", "mcp>=1.8.0", "starlette>=0.42.0", "uvicorn[standard]>=0.32.0", diff --git a/src/late/mcp/auth.py b/src/late/mcp/auth.py index a5c9160..59a7f80 100644 --- a/src/late/mcp/auth.py +++ b/src/late/mcp/auth.py @@ -4,8 +4,17 @@ from urllib.parse import urlparse import httpx +from fastmcp.server.auth import AccessToken, RemoteAuthProvider, TokenVerifier from starlette.requests import Request +from late.mcp.constants import ( + DOCS_URL, + MCP_PUBLIC_URL, + OAUTH_AUTHORIZATION_SERVER, + OAUTH_SCOPES, + SERVICE_NAME, +) + # Origin allowlist for DNS-rebinding protection (MCP spec / Anthropic Connectors # Directory requirement). Matched as exact host or subdomain suffix. Extend at # runtime with MCP_ALLOWED_ORIGINS (comma-separated hostnames). @@ -49,26 +58,6 @@ def is_allowed_origin(request: Request) -> bool: return any(host == s or host.endswith("." + s) for s in _allowed_origin_suffixes()) -def extract_late_api_key(request: Request) -> str | None: - """ - Extract Zernio API key from request Authorization header. - - Expects: Authorization: Bearer - - Function name kept as extract_late_api_key for backwards compatibility. - - Args: - request: The incoming Starlette request. - - Returns: - The extracted API key, or None if not found. - """ - auth_header = request.headers.get("Authorization") - if auth_header and auth_header.startswith("Bearer "): - return auth_header[7:] # Remove "Bearer " prefix - return None - - async def verify_late_api_key(api_key: str) -> bool: """ Verify Zernio API key by making a test request to Zernio API. @@ -91,3 +80,38 @@ async def verify_late_api_key(api_key: str) -> bool: return response.status_code == 200 except Exception: return False + + +class ZernioTokenVerifier(TokenVerifier): + """Resource-server token verification for the Zernio MCP server. + + Accepts BOTH plain Zernio API keys and OAuth access tokens: both arrive as + the same bearer string and are validated the same way — a live GET to the + Zernio API (verify_late_api_key). HTTP 200 => valid. Deliberately + format-agnostic (no JWT decode), which is why a static API key works here + just as well as an issued OAuth token. + """ + + async def verify_token(self, token: str) -> AccessToken | None: + if not await verify_late_api_key(token): + return None + return AccessToken(token=token, client_id="zernio", scopes=list(OAUTH_SCOPES)) + + +def build_auth_provider() -> RemoteAuthProvider: + """Build the FastMCP resource-server auth provider. + + RemoteAuthProvider makes this server an OAuth 2.0 resource server: it + auto-serves /.well-known/oauth-protected-resource (RFC 9728), emits the 401 + WWW-Authenticate challenge pointing clients back at that document, and + delegates token validation to ZernioTokenVerifier. The authorization server + itself (token / authorize / register) lives at zernio.com. + """ + return RemoteAuthProvider( + token_verifier=ZernioTokenVerifier(), + authorization_servers=[OAUTH_AUTHORIZATION_SERVER], + base_url=MCP_PUBLIC_URL, + scopes_supported=list(OAUTH_SCOPES), + resource_name=SERVICE_NAME, + resource_documentation=DOCS_URL, + ) diff --git a/src/late/mcp/constants.py b/src/late/mcp/constants.py index 7eb86ce..a7cf7cd 100644 --- a/src/late/mcp/constants.py +++ b/src/late/mcp/constants.py @@ -5,10 +5,11 @@ # Server information SERVICE_NAME = "Zernio MCP Server" SERVICE_VERSION = "1.2.0" -# Both transports are exposed simultaneously. SSE is kept for backwards -# compatibility with older clients; Streamable HTTP is the modern transport -# (recommended for Claude Code, mcp-remote, and any client behind a proxy -# that closes long-idle connections). +# Streamable HTTP is the primary transport (single endpoint, request/response +# with optional chunked streaming — no long-idle connection for proxies or +# bridges like mcp-remote to kill). Legacy SSE is still served for backwards +# compatibility: production continues to receive GET /sse connections from +# older client configs. New clients should use Streamable HTTP. TRANSPORT_TYPE = "sse+streamable-http" # Default server configuration @@ -22,15 +23,11 @@ # Endpoints ENDPOINT_ROOT = "/" ENDPOINT_HEALTH = "/health" -# Legacy SSE transport (two-endpoint protocol: GET /sse + POST /messages/). -# The GET /sse connection is held open for server -> client messages, which -# is the part that gets killed by idle timeouts on proxies / mcp-remote. +ENDPOINT_MCP = "/mcp" +# Legacy SSE transport (two-endpoint protocol: GET /sse + POST /messages/), +# deprecated but kept for backwards compatibility with older client configs. ENDPOINT_SSE = "/sse" ENDPOINT_MESSAGES = "/messages/" -# Modern Streamable HTTP transport (single endpoint, request/response with -# optional chunked streaming). No long-idle connection => survives proxies -# that drop idle TCP. This is the MCP-recommended transport going forward. -ENDPOINT_MCP = "/mcp" # --- OAuth 2.0 protected-resource discovery (RFC 9728 / MCP authorization spec) --- # This MCP server is a *resource server*: it does not mint tokens, it accepts @@ -38,10 +35,13 @@ # also accepts plain Zernio API keys — see verify_late_api_key). A spec- # compliant client (e.g. Claude's connector) handed only the /mcp URL discovers # the authorization server by: -# 1. reading the `resource_metadata` parameter on our 401 WWW-Authenticate, or -# 2. fetching /.well-known/oauth-protected-resource on THIS origin. -# Both must be served from the resource server's own origin, which is why this -# metadata lives here on mcp.zernio.com and not only on zernio.com. +# 1. reading the `resource_metadata` parameter on the 401 WWW-Authenticate +# challenge (emitted by FastMCP's RemoteAuthProvider), or +# 2. fetching the path-inserted discovery document that FastMCP serves at +# /.well-known/oauth-protected-resource/mcp on THIS origin. +# The pre-FastMCP server served the document at the root well-known path below; +# it is kept as a permanent redirect to the canonical document so clients still +# holding the old URL keep working (see http_server). ENDPOINT_OAUTH_PROTECTED_RESOURCE = "/.well-known/oauth-protected-resource" # Public URL of this MCP server — the OAuth `resource` identifier. Must be the @@ -69,14 +69,5 @@ "automations:write", ] -# Single source of truth for the 401 WWW-Authenticate challenge (used by both -# transports). The `resource_metadata` parameter (RFC 9728 §5.1) points clients -# at our protected-resource document so they can run OAuth discovery; clients -# that already hold a static API key just send it and skip discovery entirely. -WWW_AUTHENTICATE_BEARER = ( - 'Bearer realm="zernio-mcp", ' - f'resource_metadata="{MCP_PUBLIC_URL}{ENDPOINT_OAUTH_PROTECTED_RESOURCE}"' -) - # Documentation DOCS_URL = "https://docs.zernio.com" diff --git a/src/late/mcp/http_server.py b/src/late/mcp/http_server.py index f93fc75..6e38c1d 100644 --- a/src/late/mcp/http_server.py +++ b/src/late/mcp/http_server.py @@ -1,156 +1,192 @@ -"""HTTP server for Zernio MCP — exposes both SSE (legacy) and Streamable HTTP (modern) transports.""" +"""HTTP server for Zernio MCP — Streamable HTTP (+ legacy SSE) on FastMCP. + +Serves the FastMCP application (tool-search transform + OAuth resource-server +auth) over Streamable HTTP at /mcp. Authentication, the RFC 9728 +protected-resource metadata, and the 401 WWW-Authenticate challenge are all +provided by FastMCP's RemoteAuthProvider (see late.mcp.auth). DNS-rebinding +Origin protection is layered on as a lightweight ASGI middleware, scoped to the +transport endpoints so the public /health, / and discovery routes stay open. + +The legacy SSE transport (GET /sse + POST /messages/) is deprecated but still +served for backwards compatibility — production continues to receive SSE +connections from older client configs. Its routes come from the same FastMCP +instance (mcp.http_app(transport="sse")), so tools and auth are identical to +the Streamable HTTP endpoint. +""" import argparse -import contextlib import sys -from collections.abc import AsyncIterator import uvicorn -from mcp.server.sse import SseServerTransport -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from starlette.applications import Starlette -from starlette.routing import Mount, Route +from starlette.requests import Request +from starlette.responses import JSONResponse, RedirectResponse +from starlette.types import ASGIApp, Receive, Scope, Send +from late.mcp.auth import is_allowed_origin from late.mcp.config import ServerConfig, validate_environment from late.mcp.constants import ( + DOCS_URL, ENDPOINT_HEALTH, ENDPOINT_MCP, ENDPOINT_MESSAGES, ENDPOINT_OAUTH_PROTECTED_RESOURCE, ENDPOINT_ROOT, ENDPOINT_SSE, + SERVICE_NAME, + SERVICE_VERSION, + TRANSPORT_TYPE, ) -from late.mcp.routes import ( - create_sse_handler, - create_streamable_http_handler, - handle_health, - handle_oauth_protected_resource, - handle_root, -) +from late.mcp.server import mcp + + +@mcp.custom_route(ENDPOINT_ROOT, methods=["GET"]) +async def handle_root(_request: Request) -> JSONResponse: + """Root endpoint with server information (public, no auth).""" + return JSONResponse( + { + "service": SERVICE_NAME, + "version": SERVICE_VERSION, + "transport": TRANSPORT_TYPE, + "endpoints": { + "mcp": f"{ENDPOINT_MCP} (POST) - Streamable HTTP transport (recommended)", + "sse": f"{ENDPOINT_SSE} (GET) - SSE connection endpoint (legacy, deprecated)", + "messages": f"{ENDPOINT_MESSAGES} (POST) - SSE message handler (legacy, deprecated)", + "health": f"{ENDPOINT_HEALTH} (GET) - Health check", + }, + "documentation": DOCS_URL, + "authentication": "Required: 'Authorization: Bearer YOUR_API_KEY'", + } + ) + + +@mcp.custom_route(ENDPOINT_HEALTH, methods=["GET"]) +async def handle_health(_request: Request) -> JSONResponse: + """Health check endpoint (public, no auth).""" + return JSONResponse( + { + "status": "healthy", + "service": "zernio-mcp-http", + "version": SERVICE_VERSION, + "transport": TRANSPORT_TYPE, + } + ) + +@mcp.custom_route(ENDPOINT_OAUTH_PROTECTED_RESOURCE, methods=["GET"]) +async def handle_oauth_protected_resource_legacy(_request: Request) -> RedirectResponse: + """Legacy RFC 9728 discovery path (public, no auth). -def create_app(mcp_server, debug: bool = False) -> Starlette: + The pre-FastMCP server served the protected-resource metadata here; + FastMCP's RemoteAuthProvider serves the canonical document at the + path-inserted URL (/.well-known/oauth-protected-resource/mcp), which is + also what the 401 WWW-Authenticate challenge advertises. Permanently + redirect so clients still holding the old URL keep working. """ - Create Starlette application exposing both MCP transports. + return RedirectResponse( + f"{ENDPOINT_OAUTH_PROTECTED_RESOURCE}{ENDPOINT_MCP}", status_code=308 + ) + - Two transports are mounted simultaneously so existing SSE clients keep - working while new clients can pick the modern transport: +_ORIGIN_GUARDED_PATHS = ( + ENDPOINT_MCP.rstrip("/"), + ENDPOINT_SSE.rstrip("/"), + ENDPOINT_MESSAGES.rstrip("/"), +) - - GET /sse + POST /messages/ -> legacy SSE transport - - POST /mcp -> Streamable HTTP transport (recommended) - The Streamable HTTP session manager runs in stateless mode (each request - is fully independent). Its lifecycle is bound to the Starlette lifespan — - `session_manager.run()` must be entered before requests are served and - exited on shutdown, otherwise its internal task group is uninitialised. +class OriginGuardMiddleware: + """DNS-rebinding protection, scoped to the transport endpoints. - Args: - mcp_server: MCP server instance (the low-level `Server`, not FastMCP). - debug: Enable debug mode + Browsers attach an Origin header to cross-site requests; native MCP clients + and server-to-server callers send none (and are allowed). Only the MCP, + SSE, and SSE-message endpoints are gated — /health, / and the OAuth + discovery docs stay public. + + Implemented as a pure ASGI middleware (not BaseHTTPMiddleware) so it never + buffers the Streamable HTTP response body. + """ - Returns: - Configured Starlette application + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "http": + request = Request(scope) + path = request.url.path.rstrip("/") + guarded = any( + path == g or path.startswith(g + "/") for g in _ORIGIN_GUARDED_PATHS + ) + if guarded and not is_allowed_origin(request): + response = JSONResponse({"error": "Origin not allowed"}, status_code=403) + await response(scope, receive, send) + return + await self.app(scope, receive, send) + + +def build_app() -> Starlette: + """Build the FastMCP Streamable HTTP ASGI app with Origin protection. + + FastMCP's own host/origin checks are opened up ('*') so they never reject + production traffic (the app sits behind mcp.zernio.com / Railway); the + OriginGuardMiddleware is the sole DNS-rebinding gate, using our allowlist + (auth.is_allowed_origin, honouring MCP_ALLOWED_ORIGINS). """ - # --- Legacy SSE transport --- - sse_transport = SseServerTransport(ENDPOINT_MESSAGES) - sse_handler = create_sse_handler(mcp_server, sse_transport, debug) - - # --- Modern Streamable HTTP transport --- - # Stateless mode: each request creates a fresh transport and tears it - # down afterwards. We pick stateless because our auth model sets the API - # key in a ContextVar per-request — in stateful mode the long-running - # server task is started on the first request and would not see API keys - # set on subsequent requests. - streamable_session_manager = StreamableHTTPSessionManager( - app=mcp_server, - stateless=True, + app = mcp.http_app( + path=ENDPOINT_MCP, + transport="http", + stateless_http=True, + allowed_hosts=["*"], + allowed_origins=["*"], ) - streamable_handler = create_streamable_http_handler(streamable_session_manager, debug) - - @contextlib.asynccontextmanager - async def lifespan(_app: Starlette) -> AsyncIterator[None]: - """Drive the Streamable HTTP session manager's task group for the app's lifetime.""" - async with streamable_session_manager.run(): - yield - - return Starlette( - debug=debug, - lifespan=lifespan, - routes=[ - Route(ENDPOINT_ROOT, endpoint=handle_root, methods=["GET"]), - Route(ENDPOINT_HEALTH, endpoint=handle_health, methods=["GET"]), - # OAuth 2.0 protected-resource metadata (RFC 9728). Public, no auth. - # Lets clients given only the /mcp URL discover the zernio.com - # authorization server and run the OAuth flow. - Route( - ENDPOINT_OAUTH_PROTECTED_RESOURCE, - endpoint=handle_oauth_protected_resource, - methods=["GET"], - ), - # Streamable HTTP — single endpoint, modern transport. - # We use Route (not Mount) because Mount on "/mcp" forces a 307 - # redirect to "/mcp/", and most HTTP clients drop the request - # body / downgrade POST to GET on redirect. Route accepts an - # ASGI callable as `endpoint`, which is what we want. - # POST = client requests, GET = server-initiated streams, - # DELETE = explicit session termination (Streamable HTTP spec). - Route(ENDPOINT_MCP, endpoint=streamable_handler, methods=["GET", "POST", "DELETE"]), - # SSE (legacy) — two-endpoint protocol, kept for backwards compat. - Route(ENDPOINT_SSE, endpoint=sse_handler), - Mount(ENDPOINT_MESSAGES, app=sse_transport.handle_post_message), - ], + + # Legacy SSE transport, deprecated but kept for backwards compatibility + # (production still receives GET /sse from older client configs). Built + # from the same FastMCP instance so tools and auth are identical; only the + # /sse and /messages routes are grafted onto the main app, where they run + # under its middleware stack and shared server lifespan. + sse_app = mcp.http_app( + path=ENDPOINT_SSE, + transport="sse", + allowed_hosts=["*"], + allowed_origins=["*"], + ) + # Graft /sse, /messages, and the SSE discovery doc (the /sse 401 challenge + # advertises the path-inserted metadata for its own endpoint). + sse_paths = ( + ENDPOINT_SSE.rstrip("/"), + ENDPOINT_MESSAGES.rstrip("/"), + f"{ENDPOINT_OAUTH_PROTECTED_RESOURCE}{ENDPOINT_SSE}", + ) + app.router.routes.extend( + r for r in sse_app.routes if getattr(r, "path", "") in sse_paths ) + app.add_middleware(OriginGuardMiddleware) + return app + def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" - parser = argparse.ArgumentParser(description="Zernio MCP HTTP Server (SSE + Streamable HTTP)") + parser = argparse.ArgumentParser(description="Zernio MCP HTTP Server (Streamable HTTP)") parser.add_argument("--host", help="Host to bind to (default: 0.0.0.0)") parser.add_argument("--port", type=int, help="Port to listen on (default: 8080)") parser.add_argument("--debug", action="store_true", help="Enable debug mode") return parser.parse_args() -def print_startup_info(config: ServerConfig) -> None: - """Print server startup information.""" - print("Zernio MCP HTTP Server starting...") - print(f" Host: {config.host}") - print(f" Port: {config.port}") - print(f" Streamable HTTP endpoint (recommended): http://{config.host}:{config.port}{ENDPOINT_MCP}") - print(f" SSE endpoint (legacy): http://{config.host}:{config.port}{ENDPOINT_SSE}") - print(f" Health check: http://{config.host}:{config.port}{ENDPOINT_HEALTH}") - print(f" Debug mode: {'enabled' if config.debug else 'disabled'}") - print() - print("Ready to accept MCP connections!") - - def main() -> None: - """Entry point for the HTTP server (serves both SSE and Streamable HTTP).""" - # Validate environment + """Entry point for the HTTP server (Streamable HTTP).""" validate_environment() - - # Parse arguments args = parse_args() - - # Create configuration config = ServerConfig.from_env(host=args.host, port=args.port, debug=args.debug) - # Import and get MCP server - try: - from late.mcp.server import mcp - - mcp_server = mcp._mcp_server - except (ImportError, AttributeError) as e: - print(f"Failed to access MCP server: {e}", file=sys.stderr) - sys.exit(1) - - # Create app - app = create_app(mcp_server, debug=config.debug) + app = build_app() - # Print startup info - print_startup_info(config) + print("Zernio MCP HTTP Server starting...", file=sys.stderr) + print(f" Streamable HTTP: http://{config.host}:{config.port}{ENDPOINT_MCP}", file=sys.stderr) + print(f" Health check: http://{config.host}:{config.port}{ENDPOINT_HEALTH}", file=sys.stderr) - # Run server uvicorn.run( app, host=config.host, diff --git a/src/late/mcp/routes.py b/src/late/mcp/routes.py deleted file mode 100644 index f4259ef..0000000 --- a/src/late/mcp/routes.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Route handlers for Zernio MCP HTTP server.""" - -import sys - -from mcp.server.sse import SseServerTransport -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager -from starlette.requests import Request -from starlette.responses import JSONResponse, Response -from starlette.types import Receive, Scope, Send - -from late.mcp.auth import extract_late_api_key, is_allowed_origin, verify_late_api_key -from late.mcp.constants import ( - DOCS_URL, - ENDPOINT_HEALTH, - ENDPOINT_MCP, - ENDPOINT_MESSAGES, - ENDPOINT_SSE, - MCP_PUBLIC_URL, - OAUTH_AUTHORIZATION_SERVER, - OAUTH_SCOPES, - SERVICE_NAME, - SERVICE_VERSION, - TRANSPORT_TYPE, - WWW_AUTHENTICATE_BEARER, -) - - -async def handle_root(_request: Request) -> JSONResponse: - """Root endpoint with server information.""" - return JSONResponse( - { - "service": SERVICE_NAME, - "version": SERVICE_VERSION, - "transport": TRANSPORT_TYPE, - "endpoints": { - # Modern transport — listed first as the recommended choice. - "mcp": f"{ENDPOINT_MCP} (POST) - Streamable HTTP transport (recommended)", - # Legacy transport — kept for backwards compatibility with - # older clients that haven't migrated off SSE yet. - "sse": f"{ENDPOINT_SSE} (GET) - SSE connection endpoint (legacy)", - "messages": f"{ENDPOINT_MESSAGES} (POST) - SSE message handler (legacy)", - "health": f"{ENDPOINT_HEALTH} (GET) - Health check", - }, - "documentation": DOCS_URL, - "authentication": "Required: 'Authorization: Bearer YOUR_API_KEY'", - } - ) - - -async def handle_health(_request: Request) -> JSONResponse: - """Health check endpoint (public, no auth required).""" - return JSONResponse( - { - "status": "healthy", - "service": "zernio-mcp-http", - "version": SERVICE_VERSION, - "transport": TRANSPORT_TYPE, - } - ) - - -async def handle_oauth_protected_resource(_request: Request) -> JSONResponse: - """ - OAuth 2.0 Protected Resource Metadata (RFC 9728) — public, no auth required. - - Served from this MCP server's own origin so a client given only the `/mcp` - endpoint URL can discover the Zernio authorization server and run the OAuth - flow (this is what Claude's connector needs; without it, it can't find our - OAuth and reports "couldn't reach the server"). `resource` is THIS server's - canonical URL, not zernio.com's: RFC 8707/9728 require it to identify the - resource the client is actually talking to, and strict clients reject a - mismatch. The authorization server itself (token/authorize/register + - its own metadata) lives at zernio.com — we only point clients to it here. - """ - return JSONResponse( - { - "resource": MCP_PUBLIC_URL, - "authorization_servers": [OAUTH_AUTHORIZATION_SERVER], - "scopes_supported": OAUTH_SCOPES, - "bearer_methods_supported": ["header"], - }, - headers={"Cache-Control": "public, max-age=3600"}, - ) - - -def create_sse_handler(mcp_server, sse_transport: SseServerTransport, debug: bool = False): - """ - Create SSE connection handler. - - Args: - mcp_server: MCP server instance - sse_transport: SSE transport instance - debug: Enable debug logging - - Returns: - Async handler function - """ - - async def handle_sse(request: Request) -> Response: - """Handle SSE connection with authentication.""" - # DNS-rebinding protection: reject browser requests from un-allowlisted - # origins before doing anything else. - if not is_allowed_origin(request): - return JSONResponse({"error": "Origin not allowed"}, status_code=403) - - # 401 responses carry the shared bearer challenge (WWW_AUTHENTICATE_BEARER) - # whose `resource_metadata` parameter points clients at our OAuth - # protected-resource document — see the docstring on `_send_json_error` - # below for the full rationale. - bearer_challenge = {"WWW-Authenticate": WWW_AUTHENTICATE_BEARER} - api_key = extract_late_api_key(request) - if not api_key: - return JSONResponse( - {"error": "Missing API key. Provide via Authorization header: 'Authorization: Bearer YOUR_API_KEY'"}, - status_code=401, - headers=bearer_challenge, - ) - - # Verify API key by making test request - if not await verify_late_api_key(api_key): - return JSONResponse( - {"error": "Invalid API key"}, - status_code=401, - headers=bearer_challenge, - ) - - # Store API key in request state for use in MCP tools - request.state.late_api_key = api_key - - # Establish SSE connection - try: - # Import here to set context variable before running MCP - from late.mcp.server import set_late_api_key - - set_late_api_key(api_key) - - async with sse_transport.connect_sse( - request.scope, - request.receive, - request._send, - ) as (read_stream, write_stream): - await mcp_server.run( - read_stream, - write_stream, - mcp_server.create_initialization_options(), - ) - except Exception as e: - if debug: - print(f"SSE connection error: {e}", file=sys.stderr) - return JSONResponse( - {"error": "SSE connection failed"}, status_code=500 - ) - - return Response(status_code=200) - - return handle_sse - - -async def _send_json_error(scope: Scope, receive: Receive, send: Send, status: int, message: str) -> None: - """ - Send a JSON error response over raw ASGI. - - Used by the Streamable HTTP wrapper because the session manager expects - a raw ASGI callable, not a Starlette endpoint — so we can't just `return - JSONResponse(...)` from the wrapper. - - On 401 we add the shared bearer challenge (WWW_AUTHENTICATE_BEARER), which - carries a `resource_metadata` parameter (RFC 9728 §5.1) pointing at our - /.well-known/oauth-protected-resource document. This supports two kinds of - client without conflict: - - clients that already hold a static Zernio API key (mcp-remote with - --header, Claude Code) just send it on the retry and skip discovery; - - clients with no pre-supplied token (Claude's GUI connector) follow - `resource_metadata` to discover the zernio.com authorization server and - run the OAuth flow. The token they obtain is accepted here because - verify_late_api_key validates any bearer against the Zernio API, which - recognises both API keys and OAuth access tokens. - """ - headers = {"WWW-Authenticate": WWW_AUTHENTICATE_BEARER} if status == 401 else None - response = JSONResponse({"error": message}, status_code=status, headers=headers) - await response(scope, receive, send) - - -class StreamableHTTPAuthHandler: - """ - Auth-wrapped ASGI handler for the Streamable HTTP transport. - - Streamable HTTP is the modern MCP transport. Unlike SSE, it does not hold - a long-idle connection open — each request/response is self-contained - (with optional chunked streaming for the response). This makes it robust - against proxies and bridges (e.g. mcp-remote in Claude Code) that close - idle TCP connections after a few minutes. - - The session manager is configured in stateless mode so each request is - independent. This sidesteps the ContextVar lifetime issue we'd hit in - stateful mode: the API key set on request N would not be visible to the - long-running server task started on request 1. - - Implemented as a callable class (not a plain async function) because - Starlette's `Route` introspects callable endpoints — a plain function - gets wrapped as a `(request)`-style endpoint rather than treated as a - raw ASGI app. A class instance bypasses that detection and is invoked - directly with `(scope, receive, send)`. - """ - - def __init__(self, session_manager: StreamableHTTPSessionManager, debug: bool = False) -> None: - """ - Args: - session_manager: A StreamableHTTPSessionManager bound to the MCP - server instance. Its `.run()` lifespan must be entered by - the Starlette app — see `http_server.create_app`. - debug: Enable debug logging for handler errors. - """ - self._session_manager = session_manager - self._debug = debug - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - # Build a Request just to extract headers — we don't consume the body - # here (the session manager will do that downstream). - request = Request(scope, receive) - - # DNS-rebinding protection: reject browser requests from un-allowlisted - # origins before auth or body handling. - if not is_allowed_origin(request): - await _send_json_error(scope, receive, send, 403, "Origin not allowed") - return - - api_key = extract_late_api_key(request) - if not api_key: - await _send_json_error( - scope, - receive, - send, - 401, - "Missing API key. Provide via Authorization header: 'Authorization: Bearer YOUR_API_KEY'", - ) - return - - if not await verify_late_api_key(api_key): - await _send_json_error(scope, receive, send, 401, "Invalid API key") - return - - # Set the API key in the ContextVar before delegating. In stateless - # mode the session manager spawns a fresh per-request server task - # via anyio's task group, which propagates the current context — so - # MCP tools will see this key when they call _get_client(). - from late.mcp.server import set_late_api_key - - set_late_api_key(api_key) - - try: - await self._session_manager.handle_request(scope, receive, send) - except Exception as e: - if self._debug: - print(f"Streamable HTTP request error: {e}", file=sys.stderr) - # If the session manager already started sending a response we - # can't send another — re-raise to let the framework log it. - raise - - -def create_streamable_http_handler(session_manager: StreamableHTTPSessionManager, debug: bool = False) -> StreamableHTTPAuthHandler: - """Backwards-compatible factory wrapping `StreamableHTTPAuthHandler`.""" - return StreamableHTTPAuthHandler(session_manager, debug) diff --git a/src/late/mcp/server.py b/src/late/mcp/server.py index 4134886..f619b3a 100644 --- a/src/late/mcp/server.py +++ b/src/late/mcp/server.py @@ -30,11 +30,14 @@ from typing import Any import httpx -from mcp.server.fastmcp import FastMCP +from fastmcp import FastMCP +from fastmcp.server.dependencies import get_access_token +from fastmcp.server.transforms.search import BM25SearchTransform from mcp.types import ToolAnnotations from late import Late, MediaType, PostStatus +from .auth import build_auth_provider from .tool_definitions import TOOL_DEFINITIONS # Context variable to store the Zernio API key for the current connection @@ -45,6 +48,46 @@ _DOCS_URL = "https://docs.zernio.com/llms-full.txt" _CACHE_TTL_HOURS = 24 +# Always-visible core: the 20 hand-written ergonomic tools (guardrails + +# LLM-shaped args, preferred over their raw generated twins) plus the generated +# tools central to working on a profile — posting, the scheduling queue, +# pre-publish validation, account health, cross-platform analytics basics, and +# engagement on published posts. The remaining ~430 generated tools (messaging +# suite, ads, connect, platform-specific analytics, ...) sit behind the BM25 +# tool-search transform (search_tools / call_tool), so clients see ~51 tools up +# front instead of 481 — small enough that clients which cap or truncate large +# tool lists show them all. +_PINNED_TOOLS = [ + # Hand-written ergonomic tools + "accounts_list", "accounts_get", + "profiles_list", "profiles_get", "profiles_create", "profiles_update", "profiles_delete", + "posts_list", "posts_get", "posts_create", "posts_publish_now", "posts_cross_post", + "posts_update", "posts_delete", "posts_retry", "posts_list_failed", "posts_retry_all_failed", + "media_generate_upload_link", "media_check_upload_status", + "docs_search", + # Posting operations not covered by the ergonomic set + "posts_bulk_upload_posts", "posts_unpublish_post", "posts_edit_post", + # Scheduling queue + "queue_list_queue_slots", "queue_create_queue_slot", "queue_update_queue_slot", + "queue_delete_queue_slot", "queue_preview_queue", "queue_get_next_queue_slot", + # Pre-publish validation + "validate_post", "validate_post_length", "validate_media", + # Account health & organisation + "accounts_get_all_accounts_health", "accounts_get_account_health", + "accounts_get_follower_stats", "accounts_move_account_to_profile", + "account_groups_list_account_groups", + # Cross-platform analytics basics + "analytics_get_analytics", "analytics_get_best_time_to_post", + "analytics_get_daily_metrics", "analytics_get_post_timeline", + # Engagement on published posts + "comments_list_inbox_comments", "comments_get_inbox_post_comments", + "comments_reply_to_inbox_post", + "mentions_list_inbox_mentions", "mentions_reply_to_mention", + # Campaign tags & usage + "tracking_tags_list_tracking_tags", "tracking_tags_get_tracking_tag_stats", + "usage_get_usage", +] + # Initialize MCP server mcp = FastMCP( "Zernio", @@ -72,6 +115,12 @@ account, and retry with account_id set. Never guess. The legacy behaviour of silently picking the first matching account has been removed. """, + # Resource-server auth: validates the bearer against the Zernio API and + # auto-serves RFC 9728 protected-resource discovery (see late.mcp.auth). + auth=build_auth_provider(), + # Collapse the ~383 generated tools behind search_tools/call_tool; the + # pinned ergonomic tools remain always-visible. + transforms=[BM25SearchTransform(always_visible=_PINNED_TOOLS, max_results=8)], ) @@ -127,7 +176,8 @@ def set_late_api_key(api_key: str) -> None: """ Set the Zernio API key for the current async context. - Kept as set_late_api_key for backwards compatibility with HTTP server routes. + In HTTP mode the key now flows through FastMCP's AccessToken (see + _get_client); this setter remains for tests and embedding use. Args: api_key: The Zernio API key to use for this connection. @@ -139,7 +189,7 @@ def _get_client() -> Late: """ Get Zernio client with API key from context or environment. - For HTTP/SSE connections, uses the API key from the current context. + For HTTP connections, uses the bearer validated by the FastMCP auth layer. For STDIO connections (Claude Desktop), falls back to env vars. Priority: ZERNIO_API_KEY > LATE_API_KEY (legacy). @@ -149,17 +199,28 @@ def _get_client() -> Late: Raises: ValueError: If no API key is available. """ - # Try to get API key from context (HTTP/SSE mode) - api_key = _zernio_api_key.get() - - # Fall back to environment variables (STDIO mode for Claude Desktop) + # HTTP mode: the FastMCP auth layer validated the bearer and exposes it as + # the current request's AccessToken. get_access_token() returns None off + # HTTP (STDIO) or when unauthenticated. + api_key = "" + try: + token = get_access_token() + except Exception: + token = None + if token is not None: + api_key = token.token or "" + + # Fall back to the per-request ContextVar (kept for tests) then env vars + # (STDIO mode for Claude Desktop). + if not api_key: + api_key = _zernio_api_key.get() or "" if not api_key: api_key = os.getenv("ZERNIO_API_KEY") or os.getenv("LATE_API_KEY", "") if not api_key: raise ValueError( "Zernio API key is required. " - "For HTTP/SSE: provide via Authorization: Bearer header. " + "For HTTP: provide via Authorization: Bearer header. " "For STDIO: set ZERNIO_API_KEY environment variable." ) diff --git a/uv.lock b/uv.lock index aa61c53..4ba5491 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,48 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] + +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "caio", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + +[[package]] +name = "aiofile" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "caio", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, +] [[package]] name = "annotated-types" @@ -62,6 +104,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" }, +] + [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -71,6 +126,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "black" version = "25.12.0" @@ -115,6 +188,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, ] +[[package]] +name = "cachetools" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -396,6 +507,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] +[[package]] +name = "cyclopts" +version = "4.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/53/977540be379b0c82550df872912446def343ab0dfa138b8726120427f83d/cyclopts-4.21.0.tar.gz", hash = "sha256:477c18c791c924cca4836f79fce000a7bae45f551e340d9e1654e102c6d9ab9d", size = 190914, upload-time = "2026-07-09T19:07:07.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/f6/1c671874560a70d902dd57028a75411c176910de2df3d6a6a533de424131/cyclopts-4.21.0-py3-none-any.whl", hash = "sha256:ded3ddb15b0c815f44d245011fc4cdd5a4809a3bb8202869e9e02195a87c0e18", size = 230066, upload-time = "2026-07-09T19:07:05.326Z" }, +] + [[package]] name = "datamodel-code-generator" version = "0.43.0" @@ -426,6 +554,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -435,18 +572,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "fastmcp" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastmcp-slim", extra = ["client", "server"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/f7/5188565d1b93ad611cbd80bf473e7ad669d1f3b689c4bedcd304e1ec3472/fastmcp-3.4.4.tar.gz", hash = "sha256:378202e26ec15b23819d9a1c0d1b0ebda096bc712720532010a0b82a45c2b1df", size = 28796458, upload-time = "2026-07-09T00:32:41.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/67/3cef84ba38a23dca1e1e776bfda8a35ab3c7a6c94a8ca81d0715de6dd3c5/fastmcp-3.4.4-py3-none-any.whl", hash = "sha256:f86f208713212260068cf55c32936839eee856fefc7808e18a032f31eb0f718e", size = 8019, upload-time = "2026-07-09T00:32:39.411Z" }, +] + +[[package]] +name = "fastmcp-slim" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pydantic", extra = ["email"] }, + { name = "pydantic-settings" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/79/f35661c6a1d76dfbe17a079f912d96fffcfdd40fad5a9144bb9e7dfb1fdf/fastmcp_slim-3.4.4.tar.gz", hash = "sha256:dcaa3e0be2127d7eacdce592c2ef0039204923dc0ec396454615cb4a3275b078", size = 590203, upload-time = "2026-07-09T00:32:20.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/91/321e0b2e9ed70d0628b17ddaec76fc7b09f3e1d5d290f70bf101a2890142/fastmcp_slim-3.4.4-py3-none-any.whl", hash = "sha256:9d3a6327b9ee835188eb7323fc3b5d4cd061631b48da8ece56794bb538972505", size = 765158, upload-time = "2026-07-09T00:32:19.11Z" }, +] + +[package.optional-dependencies] +client = [ + { name = "authlib" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "opentelemetry-api" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "starlette" }, +] +server = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "joserfc" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pyperclip" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "starlette" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + [[package]] name = "genson" version = "1.3.0" @@ -456,6 +669,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, ] +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -554,6 +776,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + [[package]] name = "inflect" version = "7.5.0" @@ -585,6 +819,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -694,6 +973,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, ] +[[package]] +name = "joserfc" +version = "1.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/c6/b1cac0280f8efc57626ea8804866b37099f23cae11b1485a42b213245e31/joserfc-1.7.3.tar.gz", hash = "sha256:116955c2587139dba20621fd0bd7fc9255fa960c9fe7f43c43ebef2e801dcfcf", size = 233821, upload-time = "2026-07-08T12:41:42.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/f5/650b59d1b74f5befb7a7a7e7d7c92a26b94256df3541e2b4914152cd177a/joserfc-1.7.3-py3-none-any.whl", hash = "sha256:7c39f3f2c943dbc03122747fa8ebbd8e156e54904cf25651b452f4d2634a6075", size = 70982, upload-time = "2026-07-08T12:41:41.521Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.25.1" @@ -709,6 +1009,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -721,6 +1036,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "librt" version = "0.7.3" @@ -794,6 +1127,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -881,7 +1226,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.23.3" +version = "1.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -899,9 +1244,18 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a4/d06a303f45997e266f2c228081abe299bbcba216cb806128e2e49095d25f/mcp-1.23.3.tar.gz", hash = "sha256:b3b0da2cc949950ce1259c7bfc1b081905a51916fcd7c8182125b85e70825201", size = 600697, upload-time = "2025-12-09T16:04:37.351Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c6/13c1a26b47b3f3a3b480783001ada4268917c9f42d78a079c336da2e75e5/mcp-1.23.3-py3-none-any.whl", hash = "sha256:32768af4b46a1b4f7df34e2bfdf5c6011e7b63d7f1b0e321d0fdef4cd6082031", size = 231570, upload-time = "2025-12-09T16:04:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -987,6 +1341,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/fd/ae2da789cd923dd033c99b8d544071a827c92046b150db01cfa5cea5b3fd/openai-2.9.0-py3-none-any.whl", hash = "sha256:0d168a490fbb45630ad508a6f3022013c155a68fd708069b6a1a01a5e8f0ffad", size = 1030836, upload-time = "2025-12-04T18:15:07.063Z" }, ] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -996,6 +1374,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pathable" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1023,6 +1410,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/e2/d689d922894a7ecde73b6daeaf9b13dab5aae06fe6aaaf7514722644d382/py_key_value_aio-0.4.5.tar.gz", hash = "sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7", size = 107547, upload-time = "2026-05-27T16:37:08.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/95/b8ba862968712caa12a19666175334fa979e1f198b896a430adb3bacfe87/py_key_value_aio-0.4.5-py3-none-any.whl", hash = "sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7", size = 170005, upload-time = "2026-05-27T16:37:06.629Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aiofile", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -1047,6 +1460,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.41.5" @@ -1202,6 +1620,15 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -1259,11 +1686,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] @@ -1297,6 +1724,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1387,6 +1823,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-rst" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/d6/d0b9fafc73b65767200da027acab1db1bdb1048f4fea5ebf659df01c700e/rich_rst-2.1.0.tar.gz", hash = "sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd", size = 302732, upload-time = "2026-07-05T02:59:44.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/68/1fc93dd759605b5d00fc98b50200739e41ed32bd22d6ba35ca6c3932371b/rich_rst-2.1.0-py3-none-any.whl", hash = "sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255", size = 272987, upload-time = "2026-07-05T02:59:42.792Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -1535,6 +1997,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1558,15 +2033,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.50.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] @@ -1663,6 +2138,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "uncalled-for" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, +] + [[package]] name = "uvicorn" version = "0.38.0" @@ -1905,7 +2389,7 @@ wheels = [ [[package]] name = "zernio-sdk" -version = "1.4.229" +version = "1.4.230" source = { editable = "." } dependencies = [ { name = "httpx" }, @@ -1936,6 +2420,7 @@ dev = [ { name = "ruff" }, ] mcp = [ + { name = "fastmcp" }, { name = "mcp" }, { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, @@ -1946,6 +2431,7 @@ requires-dist = [ { name = "anthropic", marker = "extra == 'all'", specifier = ">=0.40.0" }, { name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.40.0" }, { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = ">=0.26.0" }, + { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=3.1.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "mcp", marker = "extra == 'all'", specifier = ">=1.8.0" }, { name = "mcp", marker = "extra == 'dev'", specifier = ">=1.8.0" }, @@ -1964,3 +2450,12 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], marker = "extra == 'mcp'", specifier = ">=0.32.0" }, ] provides-extras = ["ai", "anthropic", "all", "mcp", "dev"] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +]