From edc854457163a83aeb4461cc0d10bd9398b0a3d0 Mon Sep 17 00:00:00 2001 From: Arden de Raaij Date: Thu, 12 Nov 2026 18:52:23 +0000 Subject: [PATCH] Serve the coach MCP from the main image at /mcp Using the coach meant a second install: uv, a second .env with a token pasted in, a process to keep alive and a second port to expose. None of that was in the Docker quick start. The MCP now runs inside the backend process and is served at /mcp on the API's own port, so wherever the dashboard is reachable the coach is too, with nothing to configure. It still talks to the API over HTTP (loopback) with the caller's own token, so auth, user scoping and consent filtering stay one code path. A request without a bearer token is a 401. MCP_ENABLED=false turns the endpoint off. /mcp is dispatched ahead of the router and not mounted: a Starlette mount only matches /mcp/..., so a POST to the bare path fell through to the GET-only SPA catch-all and came back 405. The MCP's package is renamed app -> training_mcp, since the backend process already owns `app`, and its import-time logging setup moved into main(). It is a path dependency of the backend; fastmcp and the MCP SDK are held to the versions the MCP's own suite tests, which also keeps starlette where it was under the API. Running the MCP on its own (stdio, or a separate HTTP port) still works and is documented as the optional path. --- .dockerignore | 4 +- .env.example | 4 + CHANGELOG.md | 23 + CLAUDE.md | 43 +- README.md | 75 +- backend/Dockerfile | 6 +- backend/app/main.py | 13 +- backend/app/mcp_mount.py | 82 +++ backend/pyproject.toml | 11 + backend/tests/test_mcp_mount.py | 70 ++ backend/uv.lock | 667 ++++++++++++++++++ docker-compose.yml | 3 + mcp/app/main.py | 72 -- mcp/config/.env.example | 8 +- mcp/pyproject.toml | 6 +- mcp/tests/test_consent_filter.py | 12 +- mcp/tests/test_missed_workouts.py | 2 +- mcp/{app => training_mcp}/__init__.py | 0 .../coaching/__init__.py | 0 mcp/{app => training_mcp}/coaching/core.md | 0 .../coaching/goals/10k.md | 0 .../coaching/goals/5k.md | 0 .../coaching/goals/first_5k.md | 0 .../coaching/goals/general_fitness.md | 0 .../coaching/goals/half_marathon.md | 0 .../coaching/goals/marathon.md | 0 mcp/{app => training_mcp}/config.py | 13 +- mcp/{app => training_mcp}/consent.py | 2 +- mcp/{app => training_mcp}/instructions.py | 2 +- mcp/training_mcp/main.py | 82 +++ mcp/{app => training_mcp}/middleware.py | 4 +- mcp/{app => training_mcp}/schemas.py | 0 .../services/__init__.py | 0 .../services/api_client.py | 6 +- mcp/{app => training_mcp}/tools/__init__.py | 0 mcp/{app => training_mcp}/tools/actions.py | 6 +- mcp/{app => training_mcp}/tools/coaching.py | 4 +- mcp/{app => training_mcp}/tools/feedback.py | 6 +- .../tools/health_metrics.py | 4 +- mcp/{app => training_mcp}/tools/nutrition.py | 4 +- mcp/{app => training_mcp}/tools/plan_notes.py | 6 +- mcp/{app => training_mcp}/tools/plans.py | 6 +- mcp/{app => training_mcp}/tools/queue.py | 6 +- mcp/{app => training_mcp}/tools/workouts.py | 6 +- mcp/{app => training_mcp}/wire.py | 0 45 files changed, 1085 insertions(+), 173 deletions(-) create mode 100644 backend/app/mcp_mount.py create mode 100644 backend/tests/test_mcp_mount.py delete mode 100644 mcp/app/main.py rename mcp/{app => training_mcp}/__init__.py (100%) rename mcp/{app => training_mcp}/coaching/__init__.py (100%) rename mcp/{app => training_mcp}/coaching/core.md (100%) rename mcp/{app => training_mcp}/coaching/goals/10k.md (100%) rename mcp/{app => training_mcp}/coaching/goals/5k.md (100%) rename mcp/{app => training_mcp}/coaching/goals/first_5k.md (100%) rename mcp/{app => training_mcp}/coaching/goals/general_fitness.md (100%) rename mcp/{app => training_mcp}/coaching/goals/half_marathon.md (100%) rename mcp/{app => training_mcp}/coaching/goals/marathon.md (100%) rename mcp/{app => training_mcp}/config.py (83%) rename mcp/{app => training_mcp}/consent.py (98%) rename mcp/{app => training_mcp}/instructions.py (99%) create mode 100644 mcp/training_mcp/main.py rename mcp/{app => training_mcp}/middleware.py (92%) rename mcp/{app => training_mcp}/schemas.py (100%) rename mcp/{app => training_mcp}/services/__init__.py (100%) rename mcp/{app => training_mcp}/services/api_client.py (99%) rename mcp/{app => training_mcp}/tools/__init__.py (100%) rename mcp/{app => training_mcp}/tools/actions.py (98%) rename mcp/{app => training_mcp}/tools/coaching.py (96%) rename mcp/{app => training_mcp}/tools/feedback.py (94%) rename mcp/{app => training_mcp}/tools/health_metrics.py (95%) rename mcp/{app => training_mcp}/tools/nutrition.py (98%) rename mcp/{app => training_mcp}/tools/plan_notes.py (98%) rename mcp/{app => training_mcp}/tools/plans.py (98%) rename mcp/{app => training_mcp}/tools/queue.py (99%) rename mcp/{app => training_mcp}/tools/workouts.py (98%) rename mcp/{app => training_mcp}/wire.py (100%) diff --git a/.dockerignore b/.dockerignore index 2e8fa91..06e0cf4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,9 @@ # Build context is the repo root (backend/Dockerfile) .git docs -mcp +mcp/.venv +mcp/tests +mcp/config/.env frontend/node_modules frontend/dist backend/.venv diff --git a/.env.example b/.env.example index 892852c..b85dd76 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,10 @@ # Host port serving the API + web dashboard. #API_PORT=8001 +# The AI-coach MCP endpoint, served at /mcp on the same port (README "MCP +# Server"). Set to false to serve no coach endpoint at all. +#MCP_ENABLED=true + # Host directory the server writes its pg_dump backups into (and reports # freshness from on the admin System screen — see README "Backups"). #BACKUP_HOST_DIR=./backups diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8d2a3..bcd0187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,29 @@ tagged `X.Y.Z` and `X.Y` to GHCR (see README "Releases & upgrading"). The running server reports its version at `/api/health` and on the admin System screen. +## [Unreleased] + +### Added + +- **The coach MCP is built into the server.** It is served at `/mcp` on the + same port as the API and dashboard, from the same container, with no + configuration: wherever the dashboard is reachable, the coach is too. Each + caller still authenticates with their own token (`Authorization: Bearer`), + and a request without one gets a 401. `MCP_ENABLED=false` turns the endpoint + off. + +### Changed + +- **Running the MCP as a separate process is now optional** — only needed for + a stdio client that launches it, or to run it on a different machine. If you + run one today (default port 8590), point your clients at + `http://:8001/mcp` and retire it; nothing else changes. +- The MCP's Python package is renamed `app` → `training_mcp` so it can share a + process with the backend. `uv run start` is unaffected. A standalone + install that runs `.venv/bin/start` directly needs one `uv sync` after + pulling (the old script still points at `app.main`), and anything calling + the module path (`python -m app.main`) needs `training_mcp.main`. + ## [0.1.16] — 2026-11-12 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index f13a804..37a5bdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,8 +34,8 @@ frontend/ screens/ # one file per screen (Overview, Calendar, Workouts, Plans, Notes, Health, Queue, Settings, Users) styles/ # global design tokens + per-screen CSS mcp/ - app/ - main.py # FastMCP server entry point + training_mcp/ # Python package (NOT `app` — see MCP Server below) + main.py # FastMCP server; standalone entry point config.py # MCP settings coaching/ # Coaching playbook content (core.md + goals/*.md) + loader tools/ # MCP tool routers (workouts, queue, actions, feedback, health, plans, coaching) @@ -215,7 +215,7 @@ Models live in `backend/app/models/`. Key tables: equals the default is indistinguishable from one who never reported. The admin Users screen flags "not reported" in amber for the same reason. - **Two layers, and only together do they enforce anything.** The MCP drops - unshared tools from `tools/list` (`mcp/app/{consent,middleware}.py`) — filtering + unshared tools from `tools/list` (`mcp/training_mcp/{consent,middleware}.py`) — filtering the *advertised list*, never refusing an advertised call, because a tool that errors makes the coach apologise and tell the athlete to go enable tracking, which is the nagging the app's surfaces exist to avoid. The backend then filters @@ -239,7 +239,7 @@ Models live in `backend/app/models/`. Key tables: and is not (the inner handler has already responded by the time middleware sees the result). So domain guidance lives on the tool descriptions, which ship only when their tool does. Anything domain-specific added back to - `mcp/app/instructions.py` is read by every athlete regardless of what they + `mcp/training_mcp/instructions.py` is read by every athlete regardless of what they share — `mcp/tests/test_consent_filter.py` asserts it stays out. Related known wart: those instructions embed `date.today()` evaluated at process start, so a long-lived server keeps telling new clients the date it booted on. @@ -258,7 +258,7 @@ Models live in `backend/app/models/`. Key tables: - `GET/PUT/DELETE /api/plans/{id}/schedule` — read/set/clear a plan's recurring cadence; the response resolves it to concrete dated `sessions` and flags any that **collide with a queued run** (`warnings`, surfaced not blocked). Weekday keys validated against `mon..sun`. - `GET /api/schedule/calendar?from=&to=` — unified timeline merging scheduled runs (queue) + strength sessions (active plan schedules), each with a `conflict` flag. Shared by the dashboard **Schedule** page and the MCP. - Expansion/conflict logic is in `backend/app/schedule_utils.py` (pure) + `routes/schedule.py`. -- MCP tools (`mcp/app/tools/plans.py`): `set_strength_schedule`, `get_plan_schedule`, `clear_plan_schedule`, `get_training_calendar`. Workflow: pull routines from `hevy-mcp` → `get_training_calendar` to see runs → `set_strength_schedule` placing sessions on free days. +- MCP tools (`mcp/training_mcp/tools/plans.py`): `set_strength_schedule`, `get_plan_schedule`, `clear_plan_schedule`, `get_training_calendar`. Workflow: pull routines from `hevy-mcp` → `get_training_calendar` to see runs → `set_strength_schedule` placing sessions on free days. When adding/changing models, create a migration with `make create_migration m="description"`. Migrations auto-run on container startup. @@ -266,12 +266,35 @@ When adding/changing models, create a migration with `make create_migration m="d The MCP server (`mcp/`) exposes training data to Claude via FastMCP. It talks to the backend API over HTTP. -- Runs as a separate systemd service (`training-mcp`) on port **8590** — native FastMCP streamable HTTP at `/mcp` since 2026-07-17 (`MCP_TRANSPORT=http` in the unit, no supergateway; stdio remains the default transport for direct clients). Rollback: `training-mcp.service.pre-native.bak` -- Config: `~/.config/systemd/user/training-mcp.service` -- Env: `mcp/config/.env` (needs `TRAINING_API_URL` and `TRAINING_API_KEY`) +- **Built into the backend image, served at `/mcp` on :8001** (since 0.1.17). + `backend/app/mcp_mount.py` builds `mcp.http_app(path="/")` and `MCPDispatch`, an ASGI + wrapper added to the FastAPI app, routes `/mcp` and `/mcp/` to it; `main.py`'s lifespan + enters the MCP's own lifespan (its session manager only runs there, and only once per + process — hence the module-scoped fixture in `backend/tests/test_mcp_mount.py`). + `MCP_ENABLED=false` turns it off. Things worth not re-deriving: + - **Not `app.mount("/mcp", …)`**: a Starlette mount matches only `/mcp/…`, so a POST to + the bare `/mcp` falls to the GET-only SPA catch-all and returns 405 — the same + trailing-slash trap described under data consent. + - **The package is `training_mcp`, not `app`**, because it is imported into the backend + process, which already owns `app`. For the same reason nothing in it may do + process-wide setup at import (logging config and the missing-key warning moved into + `main()`). + - **It is a path dependency of the backend** (`[tool.uv.sources]` → `../mcp`), so the + image runs it from `backend/uv.lock` while its tests run from `mcp/uv.lock`. + `constraint-dependencies` in `backend/pyproject.toml` holds `fastmcp` and `mcp` to the + tested versions (unconstrained, the resolver also dragged starlette to 1.x under the + API). Bump both locks together. + - **Still HTTP, still the caller's token**: embedded, the MCP calls the API over loopback + (`TRAINING_API_URL` default `http://localhost:8001`) rather than touching the DB, so + auth, user scoping and the `X-Consent-Scope` filtering are one code path either way. + - The door checks only that a bearer token is *present* (401 otherwise); the API verifies + it on every call. A bogus token can therefore open a session and list tool schemas + (the consent lookup fails open to "advertise all"), but every data call fails. +- Standalone mode remains (`cd mcp && uv run start`): stdio by default, `MCP_TRANSPORT=http` + for its own port (8590). Env: `mcp/config/.env` (`TRAINING_API_URL`, `TRAINING_API_KEY`). - **Coaching playbook:** `get_coaching_playbook(goal?, experience?)` serves the running-coach methodology any LLM client should follow before creating/revising a plan (server - `instructions` direct models to call it). Content is plain markdown in `mcp/app/coaching/`: + `instructions` direct models to call it). Content is plain markdown in `mcp/training_mcp/coaching/`: `core.md` (evidence-based principles + API mapping, always included) + `goals/.md` modules (`first_5k` = C25K walk/run, `5k`, `10k`, `half_marathon`, `marathon`, `general_fitness`). Goal files may have top-level `## Beginner/Intermediate/Advanced` @@ -280,7 +303,7 @@ The MCP server (`mcp/`) exposes training data to Claude via FastMCP. It talks to Served as a tool (not an MCP prompt) deliberately: tools are the one primitive every MCP client supports, and content updates ship server-side with no client changes. - **Token passthrough (multi-user):** an `Authorization` header on the incoming MCP request is forwarded to the backend as-is, so each caller acts as their own Training API user; any presented header disables the fallback (a bad token fails, never silently downgrades). With no header, `TRAINING_API_KEY` (an athlete token, not admin) is the fallback — set `REQUIRE_AUTH_HEADER=true` in the unit/env to disable the fallback once multiple users have network access to :8590. Note: FastMCP's `get_http_headers()` strips `authorization` unless included explicitly (`include={"authorization"}`). -- **Text-only tool results (2026-07-23):** every tool is wrapped with `@text_result` (`mcp/app/wire.py`) so results ship as one compact-JSON text block — no `structuredContent`, which FastMCP otherwise duplicates the entire payload into (and which made empty lists render as `{"result": []}`). New tools must add the decorator **under** `@router.tool`. LLM-facing payload trimming lives server-side in `backend/app/workout_summary.py`: `strip_samples` (sample arrays → `samplesSummary`, segment-event pruning, float rounding) plus `downsample_timed`/`round_floats` used by the heartrate/splits endpoints; `get_training_summary` and `get_workout_heartrate` are bounded by default (`limit=52` rows / `max_samples=500`). +- **Text-only tool results (2026-07-23):** every tool is wrapped with `@text_result` (`mcp/training_mcp/wire.py`) so results ship as one compact-JSON text block — no `structuredContent`, which FastMCP otherwise duplicates the entire payload into (and which made empty lists render as `{"result": []}`). New tools must add the decorator **under** `@router.tool`. LLM-facing payload trimming lives server-side in `backend/app/workout_summary.py`: `strip_samples` (sample arrays → `samplesSummary`, segment-event pruning, float rounding) plus `downsample_timed`/`round_floats` used by the heartrate/splits endpoints; `get_training_summary` and `get_workout_heartrate` are bounded by default (`limit=52` rows / `max_samples=500`). ## Deployment diff --git a/README.md b/README.md index 4062cf2..dc8ed2f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The self-hosted backend behind **Loopback – Run Coach**, an iOS running app. I **Website: [loopback.ardenlab.dev](https://loopback.ardenlab.dev/)** -Built with FastAPI, PostgreSQL, SQLAlchemy, and React. Includes an optional MCP (Model Context Protocol) server so an AI assistant can act as your running coach over the same data. +Built with FastAPI, PostgreSQL, SQLAlchemy, and React. Includes a built-in MCP (Model Context Protocol) endpoint so an AI assistant can act as your running coach over the same data. > **This is the server half of a two-part system — you also need the [Loopback iOS app](https://github.com/aderaaij/loopback-training-app).** The app is what feeds the server: it syncs Apple HealthKit workouts and daily health metrics to it, and installs queued workouts on Apple Watch. The server runs fine on its own (API, dashboard, MCP), but without the app nothing puts data in or gets workouts onto your watch. The app installs from the **[public TestFlight beta](https://testflight.apple.com/join/fV73tucs)** (iOS 26), or builds from source. @@ -38,7 +38,7 @@ Built with FastAPI, PostgreSQL, SQLAlchemy, and React. Includes an optional MCP - **Multi-user** — Username/password accounts with per-device API tokens (`POST /api/auth/login`); all data is scoped per user - **Web dashboard** — Authenticated React SPA served same-origin by the API: overview, calendar, workouts, plans, health charts, and queue for athletes; user management and system monitoring for admins - **Admin & monitoring** — Per-user token inspection/revoke (cut off one stolen device), sync-freshness per athlete, an auth audit trail (logins, password/token/user changes), and a system screen reporting DB size and backup freshness -- **MCP server** — Let AI assistants query your training data, create workouts and plans, and correlate health metrics via natural language +- **MCP server** — Built in at `/mcp`: let AI assistants query your training data, create workouts and plans, and correlate health metrics via natural language ## Quick Start @@ -77,7 +77,7 @@ One instance serves a household: a handful of accounts, all data scoped per user 1. As `admin`, open **Users** and create an account for each athlete (role `user`). 2. Each athlete signs into the **dashboard** with their username + password. 3. In the **[iOS app](https://github.com/aderaaij/loopback-training-app)** ([TestFlight beta](https://testflight.apple.com/join/fV73tucs)), each athlete enters the server URL plus their username + password; the app logs in and stores a per-device token. -4. Optional AI-coach access: each athlete mints a token in the dashboard (**Settings → Create token**) for their own MCP client (see [MCP Server](#mcp-server-optional)). +4. Optional AI-coach access: each athlete mints a token in the dashboard (**Settings → Create token**) and points their MCP client at `/mcp` on the same address (see [MCP Server](#mcp-server)). Passwords, per-device tokens (revoke one stolen device without touching the rest), and deactivation are all managed in the dashboard. The same operations exist as a CLI fallback: @@ -165,6 +165,7 @@ Everything is configured through environment variables in the root `.env` (read | `BOOTSTRAP_ADMIN_USERNAME` | `admin` | First-run admin username | | `POSTGRES_PASSWORD` / `POSTGRES_USER` / `POSTGRES_DB` | `training-api` | Database credentials (not published outside the compose network; also fed to the app as `DATABASE_URL`) | | `API_PORT` | `8001` | Host port for the API + dashboard | +| `MCP_ENABLED` | `true` | Serve the coach MCP at `/mcp` on the same port (see [MCP Server](#mcp-server)). `false` removes the endpoint | | `BACKUP_HOST_DIR` | `./backups` | Host directory the server writes its `pg_dump` backups to (and reports freshness from) | | `BACKUP_ENABLED` | `true` | Server-managed backups: nightly dump, catch-up after downtime, pre-migration dump. `false` = bring your own (see [Backups](#backups)) | | `BACKUP_TIME` | `03:30` | Nightly backup time, container-local (set `TZ` to make it your local time) | @@ -324,48 +325,25 @@ All endpoints except `/api/health` and `/api/auth/login` require a `Bearer` toke | `GET` | `/api/nutrition/summary` | Intake, body weight and training load per period (optional: `period=week\|month`, `timezone`) | | `DELETE` | `/api/nutrition/{date}` | Remove a whole day's row | -## MCP Server (optional) +## MCP Server -The `mcp/` directory contains a [FastMCP](https://github.com/jlowin/fastmcp) server that exposes training data to AI assistants (e.g., Claude). +The server ships with an [MCP](https://modelcontextprotocol.io) endpoint that exposes training data to AI assistants (e.g., Claude), so one can act as your running coach. **It is part of the same container and needs no setup**: wherever the dashboard is reachable, the coach is at `/mcp` on the same address. -### Setup - -```bash -cd mcp -cp config/.env.example config/.env -``` - -Edit `mcp/config/.env` — set `TRAINING_API_KEY` to a token for the account the MCP should act as (mint one in the dashboard: **Settings → Create token**): - -```ini -TRAINING_API_URL=http://localhost:8001 -TRAINING_API_KEY=your-api-token-here ``` - -### Run - -```bash -uv run start +http://:8001/mcp ``` -The default transport is stdio (for direct MCP clients like Claude Desktop). Set `MCP_TRANSPORT=http` (with optional `MCP_HOST` / `MCP_PORT`) to serve streamable HTTP at `/mcp` instead — useful behind a reverse proxy or on a home server. - -**Multi-user:** an `Authorization` header on an incoming HTTP MCP request is forwarded to the backend as-is, so each caller acts as their own user. Set `REQUIRE_AUTH_HEADER=true` to disable the `TRAINING_API_KEY` fallback entirely. +Each person authenticates with their **own** token in an `Authorization: Bearer` header (mint one in the dashboard: **Settings → Create token**), so the data is scoped to their account — this is the "point your own Claude (or any LLM) at it" path. A request without a bearer token gets a 401. Set `MCP_ENABLED=false` in `.env` to serve no coach endpoint at all. ### Connect a client -Once it's serving HTTP (`MCP_TRANSPORT=http`), any MCP client can reach the -server at `http://:8590/mcp`. Each person authenticates with their -**own** token in an `Authorization: Bearer` header (mint one in the dashboard: -**Settings → Create token**), so the data is scoped to their account — this is -the "point your own Claude (or any LLM) at it" path. Substitute your tailnet -machine name and token in the snippets below. +Substitute your server's address and your token in the snippets below. **Claude Code** ```bash claude mcp add training --transport http \ - http://:8590/mcp \ + http://:8001/mcp \ --header "Authorization: Bearer " ``` @@ -378,7 +356,7 @@ claude mcp add training --transport http \ "command": "npx", "args": [ "-y", "mcp-remote", - "http://:8590/mcp", + "http://:8001/mcp", "--header", "Authorization:${AUTH_HEADER}", "--allow-http" ], @@ -394,7 +372,7 @@ claude mcp add training --transport http \ { "mcpServers": { "training": { - "url": "http://:8590/mcp", + "url": "http://:8001/mcp", "headers": { "Authorization": "Bearer " } } } @@ -416,7 +394,7 @@ claude mcp add training --transport http \ "servers": { "training": { "type": "http", - "url": "http://:8590/mcp", + "url": "http://:8001/mcp", "headers": { "Authorization": "Bearer ${input:training-token}" } } } @@ -437,6 +415,25 @@ Funnel](https://tailscale.com/kb/1223/funnel) or a reverse proxy), and claude.ai's connector flow additionally expects OAuth, which this bearer-token setup doesn't advertise. +### Running the MCP on its own + +Only needed for a client that launches the server itself over stdio, or to run +the MCP on a different machine than the API. It needs +[uv](https://docs.astral.sh/uv/). + +```bash +cd mcp +cp config/.env.example config/.env # TRAINING_API_URL + TRAINING_API_KEY +uv run start +``` + +`TRAINING_API_KEY` is a token for the account the MCP acts as when the caller +sends no `Authorization` header (always the case over stdio). The default +transport is stdio; `MCP_TRANSPORT=http` (with optional `MCP_HOST` / +`MCP_PORT`, default 8590) serves streamable HTTP at `/mcp` instead, forwarding +each caller's own `Authorization` header to the API. Set +`REQUIRE_AUTH_HEADER=true` there to disable the `TRAINING_API_KEY` fallback. + ## Development Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md). @@ -497,10 +494,10 @@ docker compose exec app python -m pytest │ ├── screens/ # One file per screen (athlete + admin) │ └── styles/ # Design tokens + per-screen CSS └── mcp/ - ├── pyproject.toml # MCP dependencies - ├── config/.env.example # MCP environment template - └── app/ - ├── main.py # FastMCP server (stdio or streamable HTTP) + ├── pyproject.toml # MCP dependencies (a path dependency of the backend) + ├── config/.env.example # Environment template for running it standalone + └── training_mcp/ # Mounted by the backend at /mcp (backend/app/mcp_mount.py) + ├── main.py # FastMCP server (+ standalone stdio / HTTP entry point) ├── tools/ # MCP tool definitions └── services/ # API client ``` diff --git a/backend/Dockerfile b/backend/Dockerfile index 8faf4af..1929148 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -26,12 +26,16 @@ ENV VIRTUAL_ENV=/opt/venv RUN uv venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" +# The coach MCP is a path dependency of the backend (../mcp in pyproject), so +# it has to sit beside /app before the sync. uv builds it into a wheel in +# /opt/venv, which is the only thing the final stage takes from here. +COPY mcp/ /mcp/ COPY backend/pyproject.toml backend/uv.lock ./ # Exactly what uv.lock pins, installed into /opt/venv. Resolving the pyproject # ranges instead is how an upstream release (fastmcp 4.0) broke CI with no # change here. --locked fails the build if the lock has drifted from # pyproject. The app isn't installed as a package (and isn't copied yet), -# hence --no-install-project. +# hence --no-install-project — which still installs the MCP path dependency. ENV UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV RUN uv sync --locked --no-install-project --extra test diff --git a/backend/app/main.py b/backend/app/main.py index 6d6d727..1e1db96 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,7 +2,7 @@ import logging import os import time -from contextlib import asynccontextmanager, suppress +from contextlib import AsyncExitStack, asynccontextmanager, suppress from pathlib import Path from fastapi import APIRouter, Depends, FastAPI, Request @@ -14,6 +14,7 @@ from app.auth_events import client_ip, record_auth_event from app.backup import run_scheduler from app.database import SessionLocal +from app.mcp_mount import MCPDispatch, mcp_app from app.rate_limit import limiter from app.routes import actions, admin, auth, config, feedback, health, health_metrics, inventory, me, nutrition, plan_notes, plans, queue, schedule, sleep, workouts from app.version import __version__ @@ -24,7 +25,12 @@ async def lifespan(_app: FastAPI): # Nightly-backup scheduler; exits on its own when backups are disabled or # the backup dir isn't writable (see app/backup.py). backup_task = asyncio.create_task(run_scheduler()) - yield + async with AsyncExitStack() as stack: + if mcp_app is not None: + # The MCP's session manager only runs inside its own lifespan; + # without this every /mcp request fails. + await stack.enter_async_context(mcp_app.lifespan(mcp_app)) + yield backup_task.cancel() with suppress(asyncio.CancelledError): await backup_task @@ -32,6 +38,9 @@ async def lifespan(_app: FastAPI): app = FastAPI(title="Training API", version=__version__, lifespan=lifespan) +# The coach MCP at /mcp, dispatched ahead of the router (see app/mcp_mount.py). +app.add_middleware(MCPDispatch) + # React dashboard build (frontend/dist), baked into the image at /app/static. # Served same-origin so the existing Tailscale Funnel setup needs no CORS. # Absent in local dev, where Vite serves the frontend and proxies /api here. diff --git a/backend/app/mcp_mount.py b/backend/app/mcp_mount.py new file mode 100644 index 0000000..d76fd1c --- /dev/null +++ b/backend/app/mcp_mount.py @@ -0,0 +1,82 @@ +"""The coach MCP, served from this process at /mcp. + +One container, one port, one URL: an install that can reach the dashboard can +reach the coach. The MCP still talks to this API over HTTP (loopback) with the +caller's own token, exactly as it does when run on its own, so every tool call +goes through the same auth, user scoping and consent filtering as any other +client — nothing here gets a side door into the database. + +Why an ASGI wrapper rather than `app.mount("/mcp", ...)`: a Starlette mount +only matches `/mcp/...`, so a POST to the bare `/mcp` (what every MCP client +sends) falls through to the SPA catch-all, a GET-only route, and comes back +405 — indistinguishable from "this server has no MCP". Dispatching ahead of +the router sidesteps route order entirely. +""" + +import json +import os +from collections.abc import Awaitable, Callable, MutableMapping +from typing import Any + +from training_mcp.main import mcp + +Scope = MutableMapping[str, Any] +Receive = Callable[[], Awaitable[MutableMapping[str, Any]]] +Send = Callable[[MutableMapping[str, Any]], Awaitable[None]] +ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] + +MCP_PATH = "/mcp" + +# On by default; MCP_ENABLED=false serves no coach endpoint at all. +MCP_ENABLED = os.environ.get("MCP_ENABLED", "true").strip().lower() not in ("0", "false", "no", "off") + +# Streamable HTTP at the sub-app's root; its lifespan (the session manager) +# is entered from the API's own lifespan in main.py. +mcp_app = mcp.http_app(path="/") if MCP_ENABLED else None + +_UNAUTHORIZED = json.dumps( + { + "detail": "The coach endpoint needs your own Loopback token: send " + "'Authorization: Bearer ' (dashboard: Settings -> Create token)." + } +).encode() + + +class MCPDispatch: + """Route /mcp to the MCP app; everything else to the API untouched.""" + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + path = scope.get("path", "") + if mcp_app is None or scope["type"] != "http" or path.rstrip("/") != MCP_PATH: + await self.app(scope, receive, send) + return + + # Presence only. The token is verified by the API on every call the + # MCP makes with it; this just turns "no credentials" into a 401 at + # the door instead of a tool error three steps into a session. + headers = dict(scope.get("headers") or []) + if not headers.get(b"authorization", b"").strip().lower().startswith(b"bearer "): + await send( + { + "type": "http.response.start", + "status": 401, + "headers": [ + (b"content-type", b"application/json"), + (b"www-authenticate", b"Bearer"), + (b"content-length", str(len(_UNAUTHORIZED)).encode()), + ], + } + ) + await send({"type": "http.response.body", "body": _UNAUTHORIZED}) + return + + # What a mount does: the prefix moves to root_path, so the sub-app + # sees its own "/" whether the client sent /mcp or /mcp/. + child = dict(scope) + child["root_path"] = scope.get("root_path", "") + MCP_PATH + child["path"] = MCP_PATH + "/" + child["raw_path"] = (MCP_PATH + "/").encode() + await mcp_app(child, receive, send) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 7b32af8..e40183f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -12,11 +12,22 @@ dependencies = [ "uvicorn>=0.34", "argon2-cffi>=23.1", "slowapi>=0.1.9", + # The coach MCP, mounted in-process at /mcp (app/mcp_mount.py). + "training-mcp", ] [project.optional-dependencies] test = ["pytest>=8"] +[tool.uv] +# The image runs the MCP from THIS lock, but its test suite runs from +# mcp/uv.lock. Hold fastmcp and the MCP SDK to the versions that suite tested; bump both +# together. +constraint-dependencies = ["fastmcp==3.1.1", "mcp==1.26.0"] + +[tool.uv.sources] +training-mcp = { path = "../mcp" } + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/backend/tests/test_mcp_mount.py b/backend/tests/test_mcp_mount.py new file mode 100644 index 0000000..f161e30 --- /dev/null +++ b/backend/tests/test_mcp_mount.py @@ -0,0 +1,70 @@ +"""The coach MCP is served from the API's own port at /mcp. + +These cover the wiring only — that a client reaches a live MCP at the bare +path and that the door asks for credentials. Tool behaviour is covered by the +MCP's own suite (mcp/tests), which runs against FastMCP's in-memory transport. +""" + +import pytest +from fastapi.testclient import TestClient + +import app.main as main_module +from app.main import app + +INITIALIZE = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": {"name": "pytest", "version": "0"}, + }, +} +HEADERS = { + "Authorization": "Bearer tapi_anything", + "Accept": "application/json, text/event-stream", +} + + +@pytest.fixture(scope="module") +def live(): + """A client inside the app's lifespan — the MCP session manager only runs + there, and can only be started once per process, hence module scope.""" + mp = pytest.MonkeyPatch() + + async def no_backups(): + return None + + # The lifespan also starts the backup scheduler; keep pg_dump out of tests. + mp.setattr(main_module, "run_scheduler", no_backups) + with TestClient(app) as c: + yield c + mp.undo() + + +# The bare path is the one every MCP client POSTs to. Without the dispatch it +# falls through to the GET-only SPA catch-all and answers 405. +@pytest.mark.parametrize("path", ["/mcp", "/mcp/"]) +def test_initialize_reaches_the_mcp(live, path): + r = live.post(path, json=INITIALIZE, headers=HEADERS) + assert r.status_code == 200, r.text + assert "training-mcp" in r.text + assert r.headers.get("mcp-session-id") + + +@pytest.mark.parametrize("auth", [None, "Basic abc", "Bearer"]) +def test_no_bearer_token_is_401(live, auth): + headers = {"Accept": HEADERS["Accept"]} + if auth: + headers["Authorization"] = auth + r = live.post("/mcp", json=INITIALIZE, headers=headers) + assert r.status_code == 401 + assert r.headers["www-authenticate"] == "Bearer" + assert "Create token" in r.json()["detail"] + + +def test_other_paths_are_untouched(live): + assert live.get("/api/health").status_code == 200 + # A path that merely starts with "mcp" is not the MCP. + assert live.post("/mcpx", json={}).status_code != 200 diff --git a/backend/uv.lock b/backend/uv.lock index 4bd8fa3..213b1a1 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -6,6 +6,24 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[manifest] +constraints = [ + { name = "fastmcp", specifier = "==3.1.1" }, + { name = "mcp", specifier = "==1.26.0" }, +] + +[[package]] +name = "aiofile" +version = "3.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/31/edb06aabd8f8f0b56d659f30800795f40b93cba96be946ce179f6931e3a5/aiofile-3.12.3.tar.gz", hash = "sha256:caa6aa746b5e47e2165f7abd741b6415e49cf4d44fddc0f61844612cc3924d41", size = 21600, upload-time = "2026-08-04T22:59:27.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/79/6e45e778c4c3cab39e0937b007b720c15f76c50c6453d153282d0fcc3588/aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017", size = 22122, upload-time = "2026-08-04T22:59:25.838Z" }, +] + [[package]] name = "alembic" version = "1.18.4" @@ -93,6 +111,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "authlib" +version = "1.9.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/96/6e7ed72bb49762a9b65d51c4cb235d793f0485cd8d6f7e9404ce955a4808/authlib-1.9.5.tar.gz", hash = "sha256:3f031d5e9b6486089e91e8f7f1b68f36275f3151c66460d650fc90d420545fd7", size = 183564, upload-time = "2026-11-05T16:13:18.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/d7/d384e7a639dd1d018767ba0fe39601b61cded066bb7af38db31ec998252d/authlib-1.9.5-py2.py3-none-any.whl", hash = "sha256:e32a63e3d869196b61816b2602231862179a2ffc29358c009d1f022c8b86a8b2", size = 267074, upload-time = "2026-11-05T16:13:16.766Z" }, +] + +[[package]] +name = "beartype" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/34/c0cfdaee9c796c8381439fed0fcab62810753b8b0adb1ec88fa7f41eaa5d/beartype-0.23.1.tar.gz", hash = "sha256:8b805f246b32931c74f2b321ea0939fe5bb2ff43c12a3eff19cc4c854b832e1d", size = 1993714, upload-time = "2026-10-11T05:36:55.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/cb/2f6aa982e2860c1156830db3da7f823b71784ed7d533ca289ea9b6c6347a/beartype-0.23.1-py3-none-any.whl", hash = "sha256:4461b4dc57e3fdd6c8a8464b22cf118002eea107c33e9fef651c69068ab3cce3", size = 1582101, upload-time = "2026-10-11T05:36:52.431Z" }, +] + +[[package]] +name = "cachetools" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/60/ac2d44dbfcca28a35151af27ad9c6236d913a67768b5bb88af90a8c02943/cachetools-8.0.0.tar.gz", hash = "sha256:5ed01bd676800d51f0025605e4fd8ad0891550513e7b0a4c3da478463764e5c3", size = 51146, upload-time = "2026-11-07T07:30:32.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bc/2e35dcb4bfb37a5dd98b8b323158d3103cf263f87164ed03702f6fc5483f/cachetools-8.0.0-py3-none-any.whl", hash = "sha256:5af562e5078f0086771a1e3832a9c3dbf5d336b6e2794a2c12a64680a4c9087d", size = 18610, upload-time = "2026-11-07T07:30:30.831Z" }, +] + +[[package]] +name = "caio" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/66/a36d1367bd5ef70b6b97b2fb502f682106555e9e49e229f35c0a49d154d5/caio-0.12.10.tar.gz", hash = "sha256:5cecc44c04d5c6291d98b61bdba999f49a7e58dff2d782b4bb0642dc6f7ecf58", size = 84118, upload-time = "2026-10-22T21:35:06.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/3e/98ddc781ae6d76407b74f33136f838e444df41a1a43d80373da01da47bbb/caio-0.12.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6b8e0724415ae86889df59af2d44fd6a531851ebc62c9cfdb1cc8235c929524d", size = 49054, upload-time = "2026-10-22T21:34:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/9851c6b24674ef72054c7b05569bd444f1358955c19ba5b5f7e15fa64be6/caio-0.12.10-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:4e72602d4c348d105c9e46dd300b85df3fd3e857e2068370847f5e29879181b5", size = 164861, upload-time = "2026-10-22T21:34:34.472Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/1f6eac05eb23284c834c08c17cc5f39fafe3754f1d2df94bcf5a2cb93179/caio-0.12.10-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:1d8b6e9275d8f9608b8ecb4b97189e7f1d0b8ed948fb552eb06518d6342a1e53", size = 162592, upload-time = "2026-10-22T21:34:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/4bb0538aa4c953c4681c7b309a9cb452eb91640e6b62f250b464f91dfad4/caio-0.12.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cff7a239334b36c4dc82a0ab69aa3244188e5c9e2c30a242d81e4502520f2b1b", size = 162324, upload-time = "2026-10-22T21:34:36.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/290190821dbc29fccb0ad96e51a7f573394df621da547cb13701de685805/caio-0.12.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a9645aeed1cdeeff34ca78442e6b274c0f801876a6bf0991724d92b7d639655", size = 162171, upload-time = "2026-10-22T21:34:38.173Z" }, + { url = "https://files.pythonhosted.org/packages/39/9d/2a2177a43901f69ef120fb0dfe4a7dc6938e57a086da3ecdeacc12a05635/caio-0.12.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:edda39ac24b4cec74486a06afe7499fc9ddb2c3d5cb98494b2c9fdf95ed7aecd", size = 42798, upload-time = "2026-10-22T21:34:39.444Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0a/5e404a09363c71dfb5f7c20234ee31dc9ad75c7072dc868b6cc5be6f9af3/caio-0.12.10-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:3b9c6e6355267f2eccd23e1a17ec2f78dce02ab00b10276999ff7a4615512952", size = 42033, upload-time = "2026-10-22T21:34:40.448Z" }, + { url = "https://files.pythonhosted.org/packages/06/94/41549be5a09a0441f1c1313d5eeb8b60b08034c89cd2c0b5cfeb6a9b9111/caio-0.12.10-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:a2b897245d5e6593a39ea9a52c7b372efc41e5a746c46a2985bdf94622f273fe", size = 165369, upload-time = "2026-10-22T21:34:41.513Z" }, + { url = "https://files.pythonhosted.org/packages/28/47/f272ce0eed9700749934823086434a94d9bea51555724188d8ee660a7fd4/caio-0.12.10-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:40e84b8679f44bee5c2b7cc7093719e15616b6752560809896dc15a8667cd69f", size = 162620, upload-time = "2026-10-22T21:34:42.671Z" }, + { url = "https://files.pythonhosted.org/packages/65/ae/644e7e72a81c4d8ce31dac9865b2a6b38fdc4c56a1418cc2d4d55591be20/caio-0.12.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccd4efddafd48018d30cd560f7c393d177fe01365ee0dcf0bc88528f220732ed", size = 162986, upload-time = "2026-10-22T21:34:43.801Z" }, + { url = "https://files.pythonhosted.org/packages/63/2a/557a90ae71f8411a2cfa0238ad70bddb60178133b7eb41e3fe8e8c854235/caio-0.12.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2138670dd6cab64bd29cfcbd6a7259c0c21646c4dc86a4ec12de907210a2aba9", size = 162258, upload-time = "2026-10-22T21:34:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/78/1a/1ffea8df50383d5bc468e94d2b45d1c458f965b80bb12ad28891d0a55266/caio-0.12.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79a4e0000dd92b1e4e7842fca99fdaa5fd0259dd33a9892741e7126e74656c49", size = 42801, upload-time = "2026-10-22T21:34:46.485Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/43da115afd82281312166711ceff5b7ca08d53f79a8955d2e22ac9968eca/caio-0.12.10-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:6fff1f609e54a2e007b6c5c0933f4782ecbf8b6f9144e0930e2dcd2e7440c637", size = 186884, upload-time = "2026-10-22T21:34:47.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5d/708aed113a2ae829a265b5ce030bab52ff0e41339cc6047f29bb0b15dfdf/caio-0.12.10-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:925b938bcdf09d1c3df2b6ddd534004b616451ef1810e47f6a63257365376b25", size = 182799, upload-time = "2026-10-22T21:34:48.86Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/8de2c1c89548902a389406560e67930333984194217d5bcef6129ff7f18a/caio-0.12.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1ebcac2a14e045aa0a4f63824264a4a845c6b1ac9074d5d673c52ef564a1ada6", size = 185097, upload-time = "2026-10-22T21:34:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/43/98997e24fa52f1e6a2a808e446693702773830a863a036a5e727024e7aa0/caio-0.12.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:39e91aab7d0a5f0cac5930562d751e9938ceef1c7ae18477a5689a4e437eada1", size = 182593, upload-time = "2026-10-22T21:34:51.256Z" }, + { url = "https://files.pythonhosted.org/packages/c3/17/78046176bc3572430d03bab1b9e82966bc5b9a28d27c41e6764a20144044/caio-0.12.10-cp315-cp315-macosx_26_0_arm64.whl", hash = "sha256:c0b52bd6fd006d2d07713025f0e1cad61ebd12342f86bfa47e2eff239fbdd01f", size = 42039, upload-time = "2026-10-22T21:34:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/14a90cfe02f14813674a1cc91ff2fad9b0d1bb98260be09b987f579baa28/caio-0.12.10-cp315-cp315-manylinux_2_34_aarch64.whl", hash = "sha256:b7a42ef7379e36e99cc6990aa62465919a3be3fb1f049a40e5aef29fbbd859e1", size = 166510, upload-time = "2026-10-22T21:34:53.703Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b1/9fbeafd6aae00cc0dae96d190cc4fe6e23d20ecd4f26e970878611761fdb/caio-0.12.10-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:89fe0e3c849a07aba1f76012a50eaf4fb2eaed0398c01bd43912a1bd0f4ab5eb", size = 163501, upload-time = "2026-10-22T21:34:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/34/81/2342f17ab96a7b5d3666cc7dccea59728c4d421e7c954e2937cd817c3be5/caio-0.12.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b04f6722884e5fb19690743772875b34306e5ba68d9ecabf7454193bb5caab", size = 164139, upload-time = "2026-10-22T21:34:56.201Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e15cfdc902a13d885fa16481c7d01ad04de7b50e0195b9dd5e219ce07614/caio-0.12.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a04ac9769fd8eddc1c0ca6308f0f6e668a86cf8d577770c11d85d0aeb584fcf4", size = 163137, upload-time = "2026-10-22T21:34:57.402Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/7fda5be8557afa8833ea65ebb29feaeeb96cb2b3373fe3220fe1e6899260/caio-0.12.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c8fc80a37f08bba4c2f108277b6eabba564f74963a1a6cab01e50f9ae4434681", size = 42800, upload-time = "2026-10-22T21:34:58.494Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e0/e2692eeb6841614f60ec099eac3c9b10a00dac74adf7321f442a2291ef8d/caio-0.12.10-cp315-cp315t-manylinux_2_34_aarch64.whl", hash = "sha256:3e3799d09b08401e83c30c671f6fa1e1227f6346a3b1336f24cb94c5a8a82b64", size = 187471, upload-time = "2026-10-22T21:34:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a8/2c3a0cd781732057ab6a8023ef0fe14017b29ef75259295df8f585e3dfba/caio-0.12.10-cp315-cp315t-manylinux_2_34_x86_64.whl", hash = "sha256:ea2e0ea832d36c1e50ab730ba304153f34eb7dc2300b1285fc7cdfae1f1f4075", size = 183526, upload-time = "2026-10-22T21:35:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/9f32db16441875afaa2a8a27d9734c2c8421f259163d7d814528160fa6c8/caio-0.12.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8d1071f8cb5d23c8aeccffe36c50a17d32257cddbf913b7e9b6fb524ba42d7a8", size = 185740, upload-time = "2026-10-22T21:35:02.059Z" }, + { url = "https://files.pythonhosted.org/packages/0d/7d/17836b477a1e57a648b90f16a27ebd367d686a02e5b2a4e72b53ae5ad7fe/caio-0.12.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a54ddadb7db4eab50bfc8b34f36fcf6763e9007f86b92f7cd03f07b542e891e", size = 183396, upload-time = "2026-10-22T21:35:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/1c44005ec4d4c3f7c51b707308efe7836ee16c466f4ff2e2a6f2089eceee/caio-0.12.10-py3-none-any.whl", hash = "sha256:b34a5483b987d207348ebbf86921bc42f08abcde833b91e0caa8760c7daa2f03", size = 26166, upload-time = "2026-10-22T21:35:04.842Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -196,6 +289,84 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, +] + +[[package]] +name = "cyclopts" +version = "5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/a2/34a66cdd2c2a5ffcfb15c195091bb944cf25bc8c1c0faeb4726ecb906c18/cyclopts-5.4.2.tar.gz", hash = "sha256:f0374ffae32bba180db7eae1d17a97a1bcd656455a3b7436f2133c4132f7c5b5", size = 257872, upload-time = "2026-11-12T13:25:49.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/03/9347dfa31741d38a02f365a5cca5b76fd1288e8f9ee0fa8120eb301e9876/cyclopts-5.4.2-py3-none-any.whl", hash = "sha256:f8a07bf41d14447960fb75d07b564b13faea1b9a7530cb669db08e43134bbff9", size = 300704, upload-time = "2026-11-12T13:25:47.475Z" }, +] + [[package]] name = "deprecated" version = "1.3.1" @@ -217,6 +388,15 @@ 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.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + [[package]] name = "email-validator" version = "2.3.0" @@ -230,6 +410,15 @@ 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" } +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 = "fastapi" version = "0.135.1" @@ -350,6 +539,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, ] +[[package]] +name = "fastmcp" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/83/c95d3bf717698a693eccb43e137a32939d2549876e884e246028bff6ecce/fastmcp-3.1.1.tar.gz", hash = "sha256:db184b5391a31199323766a3abf3a8bfbb8010479f77eca84c0e554f18655c48", size = 17347644, upload-time = "2026-03-14T19:12:20.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" }, +] + [[package]] name = "greenlet" version = "3.3.2" @@ -440,6 +661,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -458,6 +688,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[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" } +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.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[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" @@ -470,6 +742,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joserfc" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/31/5c3f27e82c92a7bbe1f028033ff19e0251540395c9be244c01a50de42bdb/joserfc-1.8.0.tar.gz", hash = "sha256:cbf03cb2c6db8cc5289f1ef8718842e53dba244b4c0de5fefa0c0eaee4bde8db", size = 241023, upload-time = "2026-11-03T10:45:57.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/7e/7ffaae13415e8f51c8e8d5d1845f2de7a27ebf2717e97a38d7e7446d6030/joserfc-1.8.0-py3-none-any.whl", hash = "sha256:176b836ac7ed9a46a88629e24076314b475f33a75d0bad7f26146e572eed7c79", size = 73147, upload-time = "2026-11-03T10:45:56.531Z" }, +] + +[[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.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[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" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +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 = "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 = "limits" version = "5.8.0" @@ -560,6 +912,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -569,6 +946,39 @@ wheels = [ { 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]] +name = "more-itertools" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz", hash = "sha256:cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63", size = 154133, upload-time = "2026-10-14T18:50:30.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl", hash = "sha256:35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0", size = 74405, upload-time = "2026-10-14T18:50:29.116Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/32/0c9bd3e4e847cd6117b64dbef4cf810faa9cdbd6689323b811569cc8a1b8/openapi_pydantic-0.6.0.tar.gz", hash = "sha256:11f3ac6ad41521fc156381ec587246b0c0ecea523bd9565ed74d3e7fac9d91cd", size = 1667049, upload-time = "2026-10-02T12:20:00.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/57/9d3eb823a03c33e9d4c810e6a3276e3a6dc820a7fdc0961db2c04e613817/openapi_pydantic-0.6.0-py3-none-any.whl", hash = "sha256:8bd6d548a250da37db96f461bcd52b3468e8834ad7d6db14b00a6b2ebbd163a3", size = 96931, upload-time = "2026-10-02T12:19:58.917Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.45.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/cf3a6b6448849656e1e5f4e2101f3988dc6789724050096aba1bc5ccf592/opentelemetry_api-1.45.2.tar.gz", hash = "sha256:d7b0008e21e5fd61cbe7a7f34ca3b6905a78ab677d9bc395e299359c75cda6b4", size = 73277, upload-time = "2026-10-29T17:29:37.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/2d/610db864d7513158cebb8f36cc88e67e73bd6c7aa1905ef18d89e1281c81/opentelemetry_api-1.45.2-py3-none-any.whl", hash = "sha256:2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34", size = 60335, upload-time = "2026-10-29T17:29:06.955Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -578,6 +988,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[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 = "platformdirs" +version = "4.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9a/8475be631fb33ca1466915e70fd594c42a4e84b512db650eaa14f90fe75d/platformdirs-4.14.5.tar.gz", hash = "sha256:32e9921db7385aa8169bee4fe9501923e5715ba9fb7ea3b9c69b0d8e0639f7a1", size = 71464, upload-time = "2026-11-04T16:54:50.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/ae/ecbbbfd0e4343a6a521d49b3c8656fb4643dee2a4f6eaf941cf3280391a8/platformdirs-4.14.5-py3-none-any.whl", hash = "sha256:8e778c6fa421516f4d3369449f8df3b0e1b72c0e0005fe88e3ec772d0a14e750", size = 35402, upload-time = "2026-11-04T16:54:48.875Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -633,6 +1061,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/c346e3474853801ec5ecf4c3ee60cfc6060327ebc31424df4e346620dde4/py_key_value_aio-0.4.6.tar.gz", hash = "sha256:267c03c3e24cb99d3097612f8a5cfd8e11785c6a2975e272db0e303ea1850bfd", size = 112876, upload-time = "2026-09-18T15:29:52.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/46/12871550fb0613d149144bb9d41f22680d5f46717bc45a76eb5798c20c3f/py_key_value_aio-0.4.6-py3-none-any.whl", hash = "sha256:820b30c7959fc738ea3d00160cdb10cd258f51e38737b2add530defeea1ded77", size = 171289, upload-time = "2026-09-18T15:29:50.665Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -751,6 +1204,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, +] + +[package.optional-dependencies] +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.1.1" @@ -785,6 +1261,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[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" @@ -821,6 +1322,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "rich" version = "14.3.3" @@ -834,6 +1348,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] +[[package]] +name = "rich-rst" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/faf7c7e36630561e3e9611730c47510cd972dd5fde8f95941ff78f76accd/rich_rst-2.2.0.tar.gz", hash = "sha256:b1e6a67f8f694a6f36035624bf73e2b1a0a4be13edaf3ba5e654d9758b61073a", size = 326270, upload-time = "2026-09-27T19:12:42.163Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/4e/716b42bf066e180c1235783b46f095a5d871d05dca7006d2295aac29c491/rich_rst-2.2.0-py3-none-any.whl", hash = "sha256:1ea1c43813dc4a8d86475fce27ffb74e0ada3d4656c9f8130ba2a59868055fe9", size = 279274, upload-time = "2026-09-27T19:12:40.548Z" }, +] + [[package]] name = "rich-toolkit" version = "0.19.7" @@ -901,6 +1428,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, ] +[[package]] +name = "rpds-py" +version = "2026.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ea/ee88fd9e756ff93fb6b1182a47ec09504a242620e33ce1d20679efefe841/rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9", size = 346229, upload-time = "2026-10-04T16:29:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/57/71/a097d6552f837500fc36e6b23d09cfb9890c3cc47531f9ca64e149799615/rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858", size = 340514, upload-time = "2026-10-04T16:29:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b7/497e85768bf4e0d8ddbaa096a4cac31d1509251dee2728a8490aa367e0b5/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063", size = 372611, upload-time = "2026-10-04T16:29:41.778Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/74ab4108916250b6e198e0d3af05bc6835eb046315f22f7a0ceb49667c5a/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025", size = 377712, upload-time = "2026-10-04T16:29:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8e/067e77d9d7b3cc793c9d909b7e97e7aadbbb1fb094093b6876902cc96d38/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5", size = 488106, upload-time = "2026-10-04T16:29:44.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b4/c5aae6c2dde269bf955f6b7d35065c655a57e47750d9668052ad67e74dda/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0", size = 390795, upload-time = "2026-10-04T16:29:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/76fab39973ee11e7f9f357c55138197bb01c86f6502cb76487e3b4f42db0/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b", size = 370794, upload-time = "2026-10-04T16:29:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/cd2a80e6d7b871937a60e935c5d507aa390d143f4ff3636f640b9733d5df/rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33", size = 375673, upload-time = "2026-10-04T16:29:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/7464a9953724e55a3b3206062fa0ffdeaa519584c6aabf65d3956d94f131/rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83", size = 398758, upload-time = "2026-10-04T16:29:50.601Z" }, + { url = "https://files.pythonhosted.org/packages/c0/86/1534b436700fd49ff411063b7c4d7e938adfabf90895b6cf1622d5a7d1f6/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c", size = 550370, upload-time = "2026-10-04T16:29:52.002Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/e1fa82a8a01e3c5820f3ba98a8b2673f642128eb368fa88871b01dd2c909/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36", size = 613106, upload-time = "2026-10-04T16:29:53.655Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/b20b8c4c3dde8755bfcd5b08492a02d0cd2e26929aedf6199ca2a377d42a/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d", size = 578235, upload-time = "2026-10-04T16:29:55.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/df3f7bbc3f7ab37a8a9db8d6c2ff2c985422899f1f7926afbf7ec3c0b8b4/rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c", size = 205293, upload-time = "2026-10-04T16:29:56.669Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/ba5a9569d719bfdd6ce863df4133ac6a1658cf1b07cc3534c31db729fbbc/rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9", size = 222409, upload-time = "2026-10-04T16:29:58.049Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/f28ca566f6c23c35bbf7445f65eb0364577b25a026305995e24f78b83d94/rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f", size = 217681, upload-time = "2026-10-04T16:29:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" }, + { url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" }, + { url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" }, + { url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" }, + { url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" }, + { url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" }, + { url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" }, + { url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/a662f367d4896dd0a10cef2fc91f10b7f08af1c10858e287e019563f338d/rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", size = 347463, upload-time = "2026-10-04T16:30:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3f/ad45d03df4f84ebae5439577ee81f3999d182711e82235c8037b6528890e/rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", size = 342051, upload-time = "2026-10-04T16:30:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/3dcd68c13d4bcc59c1f7eb33ac8e80698f06f3eb0d8a1e06419836071c20/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", size = 374143, upload-time = "2026-10-04T16:30:51.508Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0d/68c1f058a250fbd1380ebda9fc227cbf50117adf8ffe8161ef383ea79f68/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", size = 378604, upload-time = "2026-10-04T16:30:53.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d6/2d4c59b85397cb4800594fadf692688ccf5ce556adc930e7a5bf21061a5e/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", size = 490790, upload-time = "2026-10-04T16:30:54.925Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/7e05dc3aebaf52f4e026766bd668fdd09a9d0e23f64a14686b36b3501892/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", size = 393146, upload-time = "2026-10-04T16:30:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/e2e9a0a46a74ed51a0498ba1fe10f4ea6b2d9a155f372a2f91e51f18cf10/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", size = 372022, upload-time = "2026-10-04T16:30:58.295Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/8f8774df5134e23424372838bcc5c7ed4127d723e1ff52f7bebd4dcb2563/rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", size = 377092, upload-time = "2026-10-04T16:30:59.984Z" }, + { url = "https://files.pythonhosted.org/packages/90/02/8d7095d73bf9114219be40230baa5df00611e0a82ed9517779ff9c19f82b/rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", size = 400830, upload-time = "2026-10-04T16:31:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/ec/02/8206856f8f363cd042a8315dc86b3912f5dcb6d3c66bbb24b69c2bfb0775/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", size = 551478, upload-time = "2026-10-04T16:31:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/36211f1bb1f0b0313f496d92f5905b74ea107f27cb16fa3355a82f04575e/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", size = 615173, upload-time = "2026-10-04T16:31:05.281Z" }, + { url = "https://files.pythonhosted.org/packages/35/77/cda0c4a6f055446b692f0ed5692f73707cafd3dff82672ede31b1a9b59de/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", size = 579653, upload-time = "2026-10-04T16:31:07.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/ec/d8385f446240aed643b9e92a5055cff3015cc04a13c61f73b2883c478ed5/rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", size = 206498, upload-time = "2026-10-04T16:31:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a5/71b5cd00e0521e3b6b81828baea368c62b6b700ebdd9554cd7d41ddf12fa/rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", size = 223121, upload-time = "2026-10-04T16:31:10.84Z" }, + { url = "https://files.pythonhosted.org/packages/33/58/dba857c3bc8221b31b62eb170a3080f4f191de79f047200389b7ed1b06a7/rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", size = 218741, upload-time = "2026-10-04T16:31:12.592Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d0/320ab28ccc1415eeb509d68682b0014fb74690cd49f1c2d29a232475af50/rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", size = 349719, upload-time = "2026-10-04T16:31:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/56/88/f5b12f1358f443c08b7ce3cc8391d82d335f4872580e2e097847fd36087a/rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", size = 343267, upload-time = "2026-10-04T16:31:16.827Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0c/c765b0059d532acb3b9c45d781ccc22f15a96dbe443d00903f643ba9df10/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", size = 374555, upload-time = "2026-10-04T16:31:18.931Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8a/cafddfda77564a10cd21184640c3bffda6a2b8d20972fb5dedd5e0166328/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", size = 378346, upload-time = "2026-10-04T16:31:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/b9/01/5e626016eff72c183bf6c96539240cace15d402468647a453ec08415b2fc/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", size = 492373, upload-time = "2026-10-04T16:31:22.614Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9c/15a2469e9389242f46896b3f0a01d68caea8a5a35c011fcb05ef333aae73/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", size = 394371, upload-time = "2026-10-04T16:31:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/63/f5/c100ff77e1e6366e947c75969c258fdfc4b7bc5bbfe7351e62ffbf2a1228/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", size = 375132, upload-time = "2026-10-04T16:31:26.588Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/fe0269c9de253e99c81cabc12b8971a5feaa083debdaff1221e06264d9e3/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", size = 377642, upload-time = "2026-10-04T16:31:28.677Z" }, + { url = "https://files.pythonhosted.org/packages/05/65/b34a7b257baccff8f4a24a722933166d4941d5ebdc9c3f4bc4ffcd5ce4f4/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", size = 400855, upload-time = "2026-10-04T16:31:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/98/32/844e54176b6071b90b38a564e6940bc6eb8f97b2890dc709c19db9dec0f4/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", size = 552573, upload-time = "2026-10-04T16:31:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/17/73/6041d20729dffbfdf155c02d65be58bc225a1c1fb548fd87c23ef306138f/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", size = 615840, upload-time = "2026-10-04T16:31:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/418094adaee6b056ce199051b255448ed872829051e341b2294c80da0977/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", size = 582764, upload-time = "2026-10-04T16:31:36.665Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9b/1698ebf6b840ddfe8b472198abbed6ace35c6e398faeecd6c47dae742a4e/rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", size = 203795, upload-time = "2026-10-04T16:31:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +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 = "sentry-sdk" version = "2.54.0" @@ -974,6 +1595,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/43/a232e3cd804293bd4f6d01e8895bc0f6f83ea148d712baf9d783c7d93d23/sse_starlette-3.7.0.tar.gz", hash = "sha256:326416da2ab52e9a41b3f409d3ff897eda45aa68c9f3b7b17d0913aab8d219c3", size = 43869, upload-time = "2026-11-07T11:15:49.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/11/25cabac0a24fa76f58637d8671b549c90d818d69d26beecf0c59a3b186e0/sse_starlette-3.7.0-py3-none-any.whl", hash = "sha256:f1d3136caa12d129284fca3d2ded79bc9af943320b6472712955fea52a6d3eea", size = 18432, upload-time = "2026-11-07T11:15:47.86Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -997,6 +1631,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "slowapi" }, { name = "sqlalchemy" }, + { name = "training-mcp" }, { name = "uvicorn" }, ] @@ -1015,10 +1650,33 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'", specifier = ">=8" }, { name = "slowapi", specifier = ">=0.1.9" }, { name = "sqlalchemy", specifier = ">=2.0" }, + { name = "training-mcp", directory = "../mcp" }, { name = "uvicorn", specifier = ">=0.34" }, ] provides-extras = ["test"] +[[package]] +name = "training-mcp" +version = "0.1.0" +source = { directory = "../mcp" } +dependencies = [ + { name = "fastmcp" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastmcp", specifier = ">=3.1,<4" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pydantic-settings", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1" }, +] +provides-extras = ["test"] + [[package]] name = "typer" version = "0.24.1" @@ -1064,6 +1722,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "uncalled-for" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/e9/056b74ca4559f93cd38080c110696ae9a8f6d1976b162eb3638895f829e7/uncalled_for-0.6.1.tar.gz", hash = "sha256:49d9a55a806529dbb844f64a2e3adc8f44ad0b0802f5d91381a09c088c44e144", size = 71850, upload-time = "2026-11-10T21:53:02.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/d0/2db6b2299a07ddc6c6f49a39351a6faab1eb04f1c1b248a8270f1380276a/uncalled_for-0.6.1-py3-none-any.whl", hash = "sha256:1e8f86f972684db50f2888f07caa87d81a431584fa77856660e0c5e89f1cc778", size = 18957, upload-time = "2026-11-10T21:53:01.489Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" diff --git a/docker-compose.yml b/docker-compose.yml index 332c70d..959d4da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,6 +67,9 @@ services: # (that file's copies only apply to non-Docker runs). - BOOTSTRAP_ADMIN_USERNAME - BOOTSTRAP_ADMIN_PASSWORD + # Coach MCP at /mcp on this same port (on by default). Passes through + # only when set in the root .env. + - MCP_ENABLED volumes: # Backup dir: the server writes its own pg_dump files here (nightly + # pre-migration + on demand from the System screen, which also reports diff --git a/mcp/app/main.py b/mcp/app/main.py deleted file mode 100644 index 67af68e..0000000 --- a/mcp/app/main.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Training MCP Server - Main entry point.""" - -import logging -import os -from datetime import date - -from fastmcp import FastMCP - -from app import instructions -from app.config import settings -from app.middleware import ConsentMiddleware -from app.tools.actions import actions_router -from app.tools.coaching import coaching_router -from app.tools.feedback import feedback_router -from app.tools.health_metrics import health_metrics_router -from app.tools.nutrition import nutrition_router -from app.tools.plan_notes import plan_notes_router -from app.tools.plans import plans_router -from app.tools.queue import queue_router -from app.tools.workouts import workouts_router - -# Configure logging -logging.basicConfig( - level=getattr(logging, settings.log_level.upper()), - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", -) - -logger = logging.getLogger(__name__) - -mcp = FastMCP( - "training-mcp", - # Training-wide only. Anything specific to a health domain lives on the - # tool that serves it, because tools are filtered per athlete and these - # are not — see app/instructions.py. - instructions=instructions.build(date.today().isoformat()), -) - -mcp.add_middleware(ConsentMiddleware()) - -mcp.mount(coaching_router) -mcp.mount(workouts_router) -mcp.mount(queue_router) -mcp.mount(actions_router) -mcp.mount(feedback_router) -mcp.mount(health_metrics_router) -mcp.mount(nutrition_router) -mcp.mount(plans_router) -mcp.mount(plan_notes_router) - -logger.info(f"Training MCP server initialized. API URL: {settings.training_api_url}") - - -def main() -> None: - """Entry point for the MCP server. - - Default transport is stdio (direct clients, or wrapped by supergateway). - Set MCP_TRANSPORT=http (with MCP_HOST / MCP_PORT) to serve streamable HTTP - natively — no supergateway needed; point clients at http://:/mcp - """ - transport = os.environ.get("MCP_TRANSPORT", "stdio").lower() - if transport in ("http", "streamable-http"): - mcp.run( - transport="http", - host=os.environ.get("MCP_HOST", "0.0.0.0"), - port=int(os.environ.get("MCP_PORT", "8590")), - ) - else: - mcp.run() - - -if __name__ == "__main__": - main() diff --git a/mcp/config/.env.example b/mcp/config/.env.example index 8c6d519..8847648 100644 --- a/mcp/config/.env.example +++ b/mcp/config/.env.example @@ -1,4 +1,10 @@ +# Only needed when running the MCP on its own (see README "MCP Server"). +# The Docker image serves the MCP at /mcp with no configuration. + TRAINING_API_URL=http://localhost:8001 -TRAINING_API_KEY=change-me-must-match-backend-API_KEY +# A token for the account the MCP acts as when a caller sends no Authorization +# header of their own (stdio clients). Mint one in the dashboard: +# Settings -> Create token. +TRAINING_API_KEY= LOG_LEVEL=INFO REQUEST_TIMEOUT=30 diff --git a/mcp/pyproject.toml b/mcp/pyproject.toml index 719e254..42e87c8 100644 --- a/mcp/pyproject.toml +++ b/mcp/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "MCP server for Training API - query workout data and manage training queue via AI assistants" requires-python = ">=3.13" dependencies = [ - # 4.0 moved ToolResult out of fastmcp.tools.tool (imported by app/wire.py) + # 4.0 moved ToolResult out of fastmcp.tools.tool (imported by training_mcp/wire.py) # and failed the suite at import. Lift the cap as part of porting to 4. "fastmcp>=3.1,<4", "httpx>=0.27.0", @@ -16,7 +16,7 @@ dependencies = [ test = ["pytest>=8", "pytest-asyncio>=1"] [project.scripts] -start = "app.main:main" +start = "training_mcp.main:main" [tool.pytest.ini_options] # The suite drives a real MCP session over FastMCP's in-memory transport. @@ -27,4 +27,4 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["app"] +packages = ["training_mcp"] diff --git a/mcp/tests/test_consent_filter.py b/mcp/tests/test_consent_filter.py index 54181a8..635a262 100644 --- a/mcp/tests/test_consent_filter.py +++ b/mcp/tests/test_consent_filter.py @@ -11,10 +11,10 @@ import pytest -from app import consent as consent_module -from app import middleware as middleware_module -from app.consent import ACTIVITY, ALL_DOMAINS, NUTRITION, TRAINING -from app.main import mcp +from training_mcp import consent as consent_module +from training_mcp import middleware as middleware_module +from training_mcp.consent import ACTIVITY, ALL_DOMAINS, NUTRITION, TRAINING +from training_mcp.main import mcp pytestmark = pytest.mark.anyio @@ -109,7 +109,7 @@ async def test_instructions_describe_no_health_tooling(shares): every athlete regardless of what they share. Domain guidance therefore belongs on the tools (which ARE filtered) — if it creeps back in here, an athlete who shares nothing still gets told about nutrition tooling.""" - from app.instructions import build + from training_mcp.instructions import build text = build("2026-07-31") for absent in ("Health metrics tools", "Nutrition tools", "get_nutrition", @@ -133,7 +133,7 @@ async def test_domain_guidance_survives_on_the_tools(shares): def test_tool_map_survives_a_mount_prefix(): """Names are matched on the suffix too, so mounting a router under a prefix later can't quietly turn the filter into a no-op.""" - from app.consent import tool_is_shared + from training_mcp.consent import tool_is_shared for name in (METRIC_TOOL, f"health_{METRIC_TOOL}"): assert tool_is_shared(name, frozenset({ACTIVITY})) diff --git a/mcp/tests/test_missed_workouts.py b/mcp/tests/test_missed_workouts.py index e15175d..0d4263f 100644 --- a/mcp/tests/test_missed_workouts.py +++ b/mcp/tests/test_missed_workouts.py @@ -8,7 +8,7 @@ import pytest -from app.services.api_client import client +from training_mcp.services.api_client import client WORKOUT_ID = "11111111-1111-1111-1111-111111111111" diff --git a/mcp/app/__init__.py b/mcp/training_mcp/__init__.py similarity index 100% rename from mcp/app/__init__.py rename to mcp/training_mcp/__init__.py diff --git a/mcp/app/coaching/__init__.py b/mcp/training_mcp/coaching/__init__.py similarity index 100% rename from mcp/app/coaching/__init__.py rename to mcp/training_mcp/coaching/__init__.py diff --git a/mcp/app/coaching/core.md b/mcp/training_mcp/coaching/core.md similarity index 100% rename from mcp/app/coaching/core.md rename to mcp/training_mcp/coaching/core.md diff --git a/mcp/app/coaching/goals/10k.md b/mcp/training_mcp/coaching/goals/10k.md similarity index 100% rename from mcp/app/coaching/goals/10k.md rename to mcp/training_mcp/coaching/goals/10k.md diff --git a/mcp/app/coaching/goals/5k.md b/mcp/training_mcp/coaching/goals/5k.md similarity index 100% rename from mcp/app/coaching/goals/5k.md rename to mcp/training_mcp/coaching/goals/5k.md diff --git a/mcp/app/coaching/goals/first_5k.md b/mcp/training_mcp/coaching/goals/first_5k.md similarity index 100% rename from mcp/app/coaching/goals/first_5k.md rename to mcp/training_mcp/coaching/goals/first_5k.md diff --git a/mcp/app/coaching/goals/general_fitness.md b/mcp/training_mcp/coaching/goals/general_fitness.md similarity index 100% rename from mcp/app/coaching/goals/general_fitness.md rename to mcp/training_mcp/coaching/goals/general_fitness.md diff --git a/mcp/app/coaching/goals/half_marathon.md b/mcp/training_mcp/coaching/goals/half_marathon.md similarity index 100% rename from mcp/app/coaching/goals/half_marathon.md rename to mcp/training_mcp/coaching/goals/half_marathon.md diff --git a/mcp/app/coaching/goals/marathon.md b/mcp/training_mcp/coaching/goals/marathon.md similarity index 100% rename from mcp/app/coaching/goals/marathon.md rename to mcp/training_mcp/coaching/goals/marathon.md diff --git a/mcp/app/config.py b/mcp/training_mcp/config.py similarity index 83% rename from mcp/app/config.py rename to mcp/training_mcp/config.py index 60db25a..7eb6e39 100644 --- a/mcp/app/config.py +++ b/mcp/training_mcp/config.py @@ -7,11 +7,17 @@ from pydantic_settings import BaseSettings, SettingsConfigDict +# Only exists in a source checkout (mcp/config/.env). Installed as a package — +# as in the Docker image — the path is absent and configuration comes from the +# environment alone. +ENV_FILE = Path(__file__).parent.parent / "config" / ".env" + + class Settings(BaseSettings): """MCP server configuration loaded from environment variables.""" model_config = SettingsConfigDict( - env_file=str(Path(__file__).parent.parent / "config" / ".env"), + env_file=str(ENV_FILE), env_file_encoding="utf-8", extra="ignore", ) @@ -40,11 +46,6 @@ def is_configured(self) -> bool: try: settings = Settings() - if not settings.is_configured(): - print( - f"Warning: TRAINING_API_KEY not set. Expected .env file at: {Settings.model_config.get('env_file')}", - file=sys.stderr, - ) except ValidationError as e: print(f"Configuration error: {e}", file=sys.stderr) settings = Settings(training_api_key=SecretStr("")) diff --git a/mcp/app/consent.py b/mcp/training_mcp/consent.py similarity index 98% rename from mcp/app/consent.py rename to mcp/training_mcp/consent.py index f4fabd2..12629ad 100644 --- a/mcp/app/consent.py +++ b/mcp/training_mcp/consent.py @@ -20,7 +20,7 @@ import logging import time -from app.services.api_client import client +from training_mcp.services.api_client import client logger = logging.getLogger(__name__) diff --git a/mcp/app/instructions.py b/mcp/training_mcp/instructions.py similarity index 99% rename from mcp/app/instructions.py rename to mcp/training_mcp/instructions.py index a9348e2..bc466b4 100644 --- a/mcp/app/instructions.py +++ b/mcp/training_mcp/instructions.py @@ -9,7 +9,7 @@ That constraint decides where domain guidance lives. Health and nutrition guidance used to sit here *and*, in near-identical words, on the tools themselves. A tool description ships only when the tool does, and the tool list -IS filtered per session (app/middleware.py) — so the tools are the honest home +IS filtered per session (training_mcp/middleware.py) — so the tools are the honest home for it: an athlete who doesn't share nutrition has no nutrition tool, and now reads nothing about nutrition either. What stays here is what holds for every athlete: training, continuity, plans, the queue, units. diff --git a/mcp/training_mcp/main.py b/mcp/training_mcp/main.py new file mode 100644 index 0000000..f504f39 --- /dev/null +++ b/mcp/training_mcp/main.py @@ -0,0 +1,82 @@ +"""Training MCP Server - Main entry point.""" + +import logging +import os +from datetime import date + +from fastmcp import FastMCP + +from training_mcp import instructions +from training_mcp.config import ENV_FILE, settings +from training_mcp.middleware import ConsentMiddleware +from training_mcp.tools.actions import actions_router +from training_mcp.tools.coaching import coaching_router +from training_mcp.tools.feedback import feedback_router +from training_mcp.tools.health_metrics import health_metrics_router +from training_mcp.tools.nutrition import nutrition_router +from training_mcp.tools.plan_notes import plan_notes_router +from training_mcp.tools.plans import plans_router +from training_mcp.tools.queue import queue_router +from training_mcp.tools.workouts import workouts_router + +logger = logging.getLogger(__name__) + +mcp = FastMCP( + "training-mcp", + # Training-wide only. Anything specific to a health domain lives on the + # tool that serves it, because tools are filtered per athlete and these + # are not — see training_mcp/instructions.py. + instructions=instructions.build(date.today().isoformat()), +) + +mcp.add_middleware(ConsentMiddleware()) + +mcp.mount(coaching_router) +mcp.mount(workouts_router) +mcp.mount(queue_router) +mcp.mount(actions_router) +mcp.mount(feedback_router) +mcp.mount(health_metrics_router) +mcp.mount(nutrition_router) +mcp.mount(plans_router) +mcp.mount(plan_notes_router) + + +def main() -> None: + """Entry point for the standalone MCP server. + + The published Docker image does not use this: the backend mounts `mcp` + in-process at /mcp (backend/app/mcp_mount.py). This is for running the MCP + on its own — stdio for a client that launches it, or a separate HTTP port. + + Default transport is stdio (direct clients, or wrapped by supergateway). + Set MCP_TRANSPORT=http (with MCP_HOST / MCP_PORT) to serve streamable HTTP + natively — no supergateway needed; point clients at http://:/mcp + """ + # Process-wide setup lives here, not at import: imported by the backend, + # this module must not reconfigure the host process's logging. + logging.basicConfig( + level=getattr(logging, settings.log_level.upper()), + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + if not settings.is_configured() and not settings.require_auth_header: + logger.warning( + "TRAINING_API_KEY not set (expected in %s); only callers sending their own " + "Authorization header will work.", + ENV_FILE, + ) + logger.info(f"Training MCP server starting. API URL: {settings.training_api_url}") + + transport = os.environ.get("MCP_TRANSPORT", "stdio").lower() + if transport in ("http", "streamable-http"): + mcp.run( + transport="http", + host=os.environ.get("MCP_HOST", "0.0.0.0"), + port=int(os.environ.get("MCP_PORT", "8590")), + ) + else: + mcp.run() + + +if __name__ == "__main__": + main() diff --git a/mcp/app/middleware.py b/mcp/training_mcp/middleware.py similarity index 92% rename from mcp/app/middleware.py rename to mcp/training_mcp/middleware.py index f17fc2e..0033589 100644 --- a/mcp/app/middleware.py +++ b/mcp/training_mcp/middleware.py @@ -15,7 +15,7 @@ The server instructions are NOT filtered here — FastMCP fixes them when the connection opens, before middleware runs — which is why domain-specific -guidance belongs on the tools instead (see app/instructions.py). +guidance belongs on the tools instead (see training_mcp/instructions.py). """ import logging @@ -23,7 +23,7 @@ from fastmcp.server.middleware import Middleware, MiddlewareContext -from app.consent import shared_domains, tool_is_shared +from training_mcp.consent import shared_domains, tool_is_shared logger = logging.getLogger(__name__) diff --git a/mcp/app/schemas.py b/mcp/training_mcp/schemas.py similarity index 100% rename from mcp/app/schemas.py rename to mcp/training_mcp/schemas.py diff --git a/mcp/app/services/__init__.py b/mcp/training_mcp/services/__init__.py similarity index 100% rename from mcp/app/services/__init__.py rename to mcp/training_mcp/services/__init__.py diff --git a/mcp/app/services/api_client.py b/mcp/training_mcp/services/api_client.py similarity index 99% rename from mcp/app/services/api_client.py rename to mcp/training_mcp/services/api_client.py index 7f828c6..0db5ecc 100644 --- a/mcp/app/services/api_client.py +++ b/mcp/training_mcp/services/api_client.py @@ -7,7 +7,7 @@ import httpx from fastmcp.server.dependencies import get_http_headers -from app.config import settings +from training_mcp.config import settings logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ def resolve_auth(self) -> str: "' header with the request." ) if not self._api_key: - from app.config import Settings + from training_mcp.config import Settings env_file = Settings.model_config.get("env_file") raise ValueError( @@ -481,7 +481,7 @@ async def get_nutrition_summary( async def get_data_consent(self) -> dict: """Which data categories this athlete shares with the coach. - Read by app/consent.py to decide which tools to advertise. Never + Read by training_mcp/consent.py to decide which tools to advertise. Never exposed as a tool: the coach does not need to discuss the athlete's privacy settings, it just works with the tools it has. """ diff --git a/mcp/app/tools/__init__.py b/mcp/training_mcp/tools/__init__.py similarity index 100% rename from mcp/app/tools/__init__.py rename to mcp/training_mcp/tools/__init__.py diff --git a/mcp/app/tools/actions.py b/mcp/training_mcp/tools/actions.py similarity index 98% rename from mcp/app/tools/actions.py rename to mcp/training_mcp/tools/actions.py index 0358615..128bb56 100644 --- a/mcp/app/tools/actions.py +++ b/mcp/training_mcp/tools/actions.py @@ -4,7 +4,7 @@ from fastmcp import FastMCP -from app.schemas import ( +from training_mcp.schemas import ( ActivityType, IntervalBlock, JsonTolerant, @@ -13,8 +13,8 @@ WorkoutStep, dump_step, ) -from app.services.api_client import client -from app.wire import text_result +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/coaching.py b/mcp/training_mcp/tools/coaching.py similarity index 96% rename from mcp/app/tools/coaching.py rename to mcp/training_mcp/tools/coaching.py index 62bb3c9..5265605 100644 --- a/mcp/app/tools/coaching.py +++ b/mcp/training_mcp/tools/coaching.py @@ -11,8 +11,8 @@ from fastmcp import FastMCP -from app.coaching import load_playbook -from app.wire import text_result +from training_mcp.coaching import load_playbook +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/feedback.py b/mcp/training_mcp/tools/feedback.py similarity index 94% rename from mcp/app/tools/feedback.py rename to mcp/training_mcp/tools/feedback.py index 75c4720..b3669c1 100644 --- a/mcp/app/tools/feedback.py +++ b/mcp/training_mcp/tools/feedback.py @@ -4,9 +4,9 @@ from fastmcp import FastMCP -from app.schemas import FeedbackAction -from app.services.api_client import client -from app.wire import text_result +from training_mcp.schemas import FeedbackAction +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/health_metrics.py b/mcp/training_mcp/tools/health_metrics.py similarity index 95% rename from mcp/app/tools/health_metrics.py rename to mcp/training_mcp/tools/health_metrics.py index a7b12d3..69fee6e 100644 --- a/mcp/app/tools/health_metrics.py +++ b/mcp/training_mcp/tools/health_metrics.py @@ -4,8 +4,8 @@ from fastmcp import FastMCP -from app.services.api_client import client -from app.wire import text_result +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/nutrition.py b/mcp/training_mcp/tools/nutrition.py similarity index 98% rename from mcp/app/tools/nutrition.py rename to mcp/training_mcp/tools/nutrition.py index eff0562..95bf557 100644 --- a/mcp/app/tools/nutrition.py +++ b/mcp/training_mcp/tools/nutrition.py @@ -4,8 +4,8 @@ from fastmcp import FastMCP -from app.services.api_client import client -from app.wire import text_result +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/plan_notes.py b/mcp/training_mcp/tools/plan_notes.py similarity index 98% rename from mcp/app/tools/plan_notes.py rename to mcp/training_mcp/tools/plan_notes.py index 4e8432d..9325c93 100644 --- a/mcp/app/tools/plan_notes.py +++ b/mcp/training_mcp/tools/plan_notes.py @@ -17,9 +17,9 @@ from pydantic import Field -from app.schemas import NoteKind -from app.services.api_client import client -from app.wire import text_result +from training_mcp.schemas import NoteKind +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/plans.py b/mcp/training_mcp/tools/plans.py similarity index 98% rename from mcp/app/tools/plans.py rename to mcp/training_mcp/tools/plans.py index dd633a4..585f995 100644 --- a/mcp/app/tools/plans.py +++ b/mcp/training_mcp/tools/plans.py @@ -5,9 +5,9 @@ from fastmcp import FastMCP -from app.schemas import JsonTolerant, PlanStatus, WeeklyDays -from app.services.api_client import client -from app.wire import text_result +from training_mcp.schemas import JsonTolerant, PlanStatus, WeeklyDays +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/queue.py b/mcp/training_mcp/tools/queue.py similarity index 99% rename from mcp/app/tools/queue.py rename to mcp/training_mcp/tools/queue.py index a72bedf..71a1f28 100644 --- a/mcp/app/tools/queue.py +++ b/mcp/training_mcp/tools/queue.py @@ -5,7 +5,7 @@ from fastmcp import FastMCP -from app.schemas import ( +from training_mcp.schemas import ( ActivityType, BatchWorkoutItem, IntervalBlock, @@ -16,8 +16,8 @@ WorkoutStep, dump_step, ) -from app.services.api_client import client -from app.wire import text_result +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/tools/workouts.py b/mcp/training_mcp/tools/workouts.py similarity index 98% rename from mcp/app/tools/workouts.py rename to mcp/training_mcp/tools/workouts.py index 68fb1ad..cbdfbf4 100644 --- a/mcp/app/tools/workouts.py +++ b/mcp/training_mcp/tools/workouts.py @@ -4,9 +4,9 @@ from fastmcp import FastMCP -from app.schemas import SummaryPeriod -from app.services.api_client import client -from app.wire import text_result +from training_mcp.schemas import SummaryPeriod +from training_mcp.services.api_client import client +from training_mcp.wire import text_result logger = logging.getLogger(__name__) diff --git a/mcp/app/wire.py b/mcp/training_mcp/wire.py similarity index 100% rename from mcp/app/wire.py rename to mcp/training_mcp/wire.py