diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0e8ea90..74fc6cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,11 @@ updates: - "/.github/actions/*" schedule: interval: "daily" + + - package-ecosystem: "pip" + directory: "/packages/auth0-ai-mcp" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ef6d46b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Build and Test + +on: + merge_group: + pull_request: + types: + - opened + - synchronize + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + test: + name: test (${{ matrix.package }}, py${{ matrix.python-version }}) + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ matrix.package }} + strategy: + matrix: + package: [packages/auth0-ai-mcp] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@a783c322200f0519c7926aa6faa857c4e23e9263 # v1.4.2 + with: + version: "2.4.1" + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ./${{ matrix.package }}/.venv + key: venv-${{ runner.os }}-${{ matrix.package }}-${{ matrix.python-version }}-${{ hashFiles(format('{0}/poetry.lock', matrix.package)) }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install package + run: poetry install --no-interaction + + - name: Run tests with pytest + run: | + poetry run pytest -v + + - name: Run ruff linting + run: | + poetry run ruff check . diff --git a/packages/auth0-ai-mcp/.gitignore b/packages/auth0-ai-mcp/.gitignore new file mode 100644 index 0000000..9af0e1b --- /dev/null +++ b/packages/auth0-ai-mcp/.gitignore @@ -0,0 +1,30 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +#Environments +.env +.venv +.venv-* +env/ + +#Session Cache +.sessions_cache +.DS_Store + +#Build files +dist + +#Test and lint caches +.pytest_cache/ +.ruff_cache/ +.coverage +coverage.xml + +# IDE +.idea/ + +# AI Tools +.claude/ diff --git a/packages/auth0-ai-mcp/.ruff.toml b/packages/auth0-ai-mcp/.ruff.toml new file mode 100644 index 0000000..b9a4b9d --- /dev/null +++ b/packages/auth0-ai-mcp/.ruff.toml @@ -0,0 +1,21 @@ +line-length = 100 +target-version = "py310" + +[lint] +preview = true +explicit-preview-rules = true # only the preview rules selected below, such as PLC0415 +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "S", # bandit (security) + "PLC0415", # import outside top level +] +ignore = ["E501", "B904"] # Line too long, Exception handling without from + +[lint.per-file-ignores] +"tests/*" = ["S101", "S105", "S106"] # Allow assert and ignore hardcoded password warnings in test files diff --git a/packages/auth0-ai-mcp/.shiprc b/packages/auth0-ai-mcp/.shiprc new file mode 100644 index 0000000..de867c4 --- /dev/null +++ b/packages/auth0-ai-mcp/.shiprc @@ -0,0 +1,7 @@ +{ + "files": { + ".version": [], + "pyproject.toml": ["version = \"{MAJOR}.{MINOR}.{PATCH}{PRERELEASE_LABEL}{PRERELEASE_COUNTER}\""] + }, + "prefixVersion": false +} diff --git a/packages/auth0-ai-mcp/.snyk b/packages/auth0-ai-mcp/.snyk new file mode 100644 index 0000000..69a0a4f --- /dev/null +++ b/packages/auth0-ai-mcp/.snyk @@ -0,0 +1,17 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.12.0 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + "snyk:lic:pip:certifi:MPL-2.0": + - '*': + reason: "Accepting certifi's MPL-2.0 license for now" + expires: "2030-12-31T23:59:59Z" + "snyk:lic:pip:jwcrypto:LGPL-3.0": + - '*': + reason: "Accepting jwcrypto's LGPL-3.0 license for now" + expires: "2030-12-31T23:59:59Z" + "snyk:lic:pip:cffi:Unknown": + - '*': + reason: "Accepting the Unknown license for now" + expires: "2030-12-31T23:59:59Z" +patch: {} diff --git a/packages/auth0-ai-mcp/.version b/packages/auth0-ai-mcp/.version new file mode 100644 index 0000000..cdf2991 --- /dev/null +++ b/packages/auth0-ai-mcp/.version @@ -0,0 +1 @@ +1.0.0b0 diff --git a/packages/auth0-ai-mcp/CHANGELOG.md b/packages/auth0-ai-mcp/CHANGELOG.md new file mode 100644 index 0000000..420e6f2 --- /dev/null +++ b/packages/auth0-ai-mcp/CHANGELOG.md @@ -0,0 +1 @@ +# Change Log diff --git a/packages/auth0-ai-mcp/CONTRIBUTING.md b/packages/auth0-ai-mcp/CONTRIBUTING.md new file mode 100644 index 0000000..15a1600 --- /dev/null +++ b/packages/auth0-ai-mcp/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contribution + +Please read [Auth0's contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md). + +## Environment setup + +- Python 3.10 or newer and Poetry +- Run `poetry install` to install dependencies +- Follow the local development steps below to get started + +## Local development + +- `poetry install`: install dependencies +- `poetry run pytest`: run unit tests +- `poetry run ruff check .`: check code style and lint errors +- `poetry build`: build the package + +## Testing + +### Adding tests + +Every change should be accompanied by a test. + +### Running tests + +Run unit tests and the linter before opening a PR: + +```bash +poetry run pytest +poetry run ruff check . +``` \ No newline at end of file diff --git a/packages/auth0-ai-mcp/LICENSE b/packages/auth0-ai-mcp/LICENSE new file mode 100644 index 0000000..e6815d1 --- /dev/null +++ b/packages/auth0-ai-mcp/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/packages/auth0-ai-mcp/NOTICE b/packages/auth0-ai-mcp/NOTICE new file mode 100644 index 0000000..c224642 --- /dev/null +++ b/packages/auth0-ai-mcp/NOTICE @@ -0,0 +1,18 @@ +============================================================= + Declared License +============================================================= +Apache-2.0 +============================================================= + First Party Licenses + +Copyright 2026 Okta, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +============================================================= + Dependencies +============================================================= + +This software has dependencies that do not require attribution notices in this file as none of them are bundled with this distribution. +These dependencies are installed separately by package managers. For a complete list of dependencies and their licenses, please refer to the +pyproject.toml file for this package. \ No newline at end of file diff --git a/packages/auth0-ai-mcp/README.md b/packages/auth0-ai-mcp/README.md new file mode 100644 index 0000000..01e4153 --- /dev/null +++ b/packages/auth0-ai-mcp/README.md @@ -0,0 +1,132 @@ +The `auth0-ai-mcp` library secures [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers with Auth0, built on [`auth0-api-python`](https://github.com/auth0/auth0-api-python). + +An MCP server is just another resource server. This library ships no MCP framework of its own. It verifies the incoming bearer token, runs the On-Behalf-Of token exchange for downstream API calls with an optional cache in front, and exposes the verified caller to tool code. Each framework gets a thin adapter that wires this into its request pipeline. + +![Release](https://img.shields.io/pypi/v/auth0-ai-mcp) ![Downloads](https://img.shields.io/pypi/dw/auth0-ai-mcp) [![License](https://img.shields.io/:license-Apache--2.0-blue.svg?style=flat)](https://opensource.org/licenses/Apache-2.0) + +🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback) + +## How it works + +1. **Verify** - every incoming bearer token is checked before the MCP handler runs. +2. **Context** - the verified caller is available to tool code. +3. **Scopes** - tools can require scopes on the caller's token. +4. **On-Behalf-Of** - exchange the caller's token for a downstream token. +5. **Token Vault** - exchange the caller's token for a federated provider token. +6. **Client Credentials** - get a token for the server itself. + +## Getting Started + +### 1. Install + +```bash +pip install auth0-ai-mcp +``` + +Or with Poetry: + +```bash +poetry add auth0-ai-mcp +``` + +Python 3.10 or later is required. The examples below use the official `mcp` SDK, so install it with the `mcp` extra: + +```bash +pip install "auth0-ai-mcp[mcp]" +``` + +The snippets use the 2.x import `mcp.server.mcpserver` and were tested with `mcp` 2.3. + +### 2. Build the client + +```python +from auth0_ai_mcp import create_mcp_client + +client = create_mcp_client( + domain="your-tenant.auth0.com", + audience="https://your-mcp-server/mcp", + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", +) +``` + +> [!IMPORTANT] +> `audience` must be the public URL of your MCP server. It is also the API identifier you register in Auth0, so a token issued for any other value is rejected with a `401`. + +`client_id` and `client_secret` are only needed for token exchanges. + +### 3. Wire an adapter + +The ASGI adapter wraps any ASGI app and handles all auth work before the MCP handler runs: + +```python +from mcp.server.mcpserver import MCPServer +from mcp.server.transport_security import TransportSecuritySettings + +from auth0_ai_mcp.middleware import Auth0AsgiMiddleware + +mcp = MCPServer("weather") + +secured_app = Auth0AsgiMiddleware( + mcp.streamable_http_app( + transport_security=TransportSecuritySettings(allowed_hosts=["your-mcp-server", "your-mcp-server:*"]), + ), + mcp_client=client, + scopes_supported=["read:weather"], +) +``` + +The `mcp` SDK only accepts requests for `127.0.0.1`, `localhost` and `::1` by default, so list your public host in `allowed_hosts`, with and without a port, or every request gets a `421`. `scopes_supported` is advertised in the metadata, so clients know to request the scopes your tools need. Without it a client may ask for no scope and every gated tool fails. + +### 4. Write a gated tool + +```python +from auth0_ai_mcp import require_caller_mcp_client, with_required_scopes + +@mcp.tool() +@with_required_scopes("read:weather") +async def get_weather(location: str) -> dict: + auth0 = require_caller_mcp_client() + token = await auth0.get_token_on_behalf_of( + audience="https://weather-api/", + scope="read:weather", + ) + # call the downstream API with token["access_token"] + ... +``` + +`with_required_scopes` raises before the tool body runs if the caller's token is missing a listed scope. Keep `@mcp.tool()` as the top decorator. If it sits below, the framework registers the unguarded function and the check is skipped. `require_caller_mcp_client` reads the request-scoped client from the `contextvar` set by the adapter. + +## Feedback + +### Contributing + +We appreciate feedback and contribution to this repo! Before you get started, please read the following: + +- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) +- [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) +- [This package's contribution guide](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/CONTRIBUTING.md) + +### Raise an issue + +To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-ai-python/issues). + +## Vulnerability Reporting + +Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. + +## What is Auth0? + +

+ + + + Auth0 Logo + +

+

+ Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout Why Auth0? +

+

+ This project is licensed under the Apache 2.0 license. See the LICENSE file for more info. +

diff --git a/packages/auth0-ai-mcp/poetry.lock b/packages/auth0-ai-mcp/poetry.lock new file mode 100644 index 0000000..6163d8e --- /dev/null +++ b/packages/auth0-ai-mcp/poetry.lock @@ -0,0 +1,1735 @@ +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. + +[[package]] +name = "ada-url" +version = "1.32.0" +description = "URL parser and manipulator based on the WHAT WG URL standard" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "ada_url-1.32.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:c4aecd49bea562c28e7b2fe03257e88cee8f5af9f8a02c21f4deabeb341d1399"}, + {file = "ada_url-1.32.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:3ad2c40dcf5ecf4f72ef2fb8c5edcecbaf5f1ea19418b416bd2c1785c35d7d32"}, + {file = "ada_url-1.32.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40e3e88079bfddf303d72165fbc8453f10d0f9431753e43345135305cbfad044"}, + {file = "ada_url-1.32.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9f5e11b8939bc66b8615262396fc0a630e5a796a6702bda253121c30ee873f9"}, + {file = "ada_url-1.32.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5969f420ccfc4439233fca31cdcd5b0d1ba1cb165b5fb49767891ab3b247e3a0"}, + {file = "ada_url-1.32.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a64b58055b468d4d2d6f79969da25157b420650e24eb3abb5732005f2657cc83"}, + {file = "ada_url-1.32.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ab3576f6d08a8dceac382bffb570b0a176e6514f9ad829929ce58c16a68d0d"}, + {file = "ada_url-1.32.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d7288aa9d8b46b4aa2933142a3ca1c512c0c60f2ac521b9b9e3edc7522be2e3"}, + {file = "ada_url-1.32.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:2a8a5535e103e4a8b5df004bf7bdc9972a7c91bb1b216c9fb13d7553584e8e48"}, + {file = "ada_url-1.32.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c0e0d09ba016f5ceec31fd3146695977f398ac6138dbb3fb71b8dd6dd5939d3"}, + {file = "ada_url-1.32.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df220a146fb76a222b5517ece6db0435809ab4d084fc6f7c6d1c33306bc42fc6"}, + {file = "ada_url-1.32.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac765be12f6725445c42830ada8cb9a771ac6a53775f994e0b8127b1ec7a8e5"}, + {file = "ada_url-1.32.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba92322e725ca54c7e363bfd3ea2e037c00a0328816d569e7aca5f0ba10cfb89"}, + {file = "ada_url-1.32.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b81e4dc35a0e8b66033858f736cdc6469c3b7bc3007ad6983955d92cc31dfde4"}, + {file = "ada_url-1.32.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47bbfe5ac26522008233dc18c8ad781684e03e3817c0b8af2038426b888a38f6"}, + {file = "ada_url-1.32.0-cp311-cp311-win_amd64.whl", hash = "sha256:647624cd77e6a8d825e678e567d7c8665c0e81e9a497ddd37251803b040ae434"}, + {file = "ada_url-1.32.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:a3b5afe0ad3c9123ecc504f12aa04ee3a281b4edd34333488b8a6d4a901a740d"}, + {file = "ada_url-1.32.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:6e7712dd34f674c372d9315cd57fc1e342437090a19eaa6aa775cdd473869330"}, + {file = "ada_url-1.32.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5381328ee3fb1290538e621b0dfe1f842dbbfc7bb06d1e4f0eb10d3ce24474a7"}, + {file = "ada_url-1.32.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ab2bc09336ad8ccffca6ea86d149430413007baf8b9684fad8a50ec270391ec"}, + {file = "ada_url-1.32.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aee8ebca59d5627eecf22fd73889bb0f4415e64203d14c9557399c20c0aff549"}, + {file = "ada_url-1.32.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c17dbcda7da6a4122b14d9cb612af07b78eaf2ba414d7f9069fd6e8723ed0ffa"}, + {file = "ada_url-1.32.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f5d5933bf8bd017edd5ad2c5c8ecddfb348b5642c49aad1aa565c8ff3ca02e89"}, + {file = "ada_url-1.32.0-cp312-cp312-win_amd64.whl", hash = "sha256:26b2ab11506a6fde3911789664ae5583b02422a1fb21d15ca4bf2ab2fe9efebd"}, + {file = "ada_url-1.32.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:18b14441d9cb39680ef24dacf49d9266ce3326b14a11df890fee39706fe726fd"}, + {file = "ada_url-1.32.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3c83f01a2e2e3cb64532f59cf1cee7960a44f393888ab98b4f8d56629b2e0fbe"}, + {file = "ada_url-1.32.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f6efa3421e20d1cd5d5d849b22282b676a4cff256228486f862dce59ade11d9"}, + {file = "ada_url-1.32.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b50e107ef029e27cf95efdfde942dc994cef18c33140a340f2464d4a34aae9d"}, + {file = "ada_url-1.32.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9320cf887dac87dd81e9d571921c79d2b53922c34c25a579861d092c129eff1d"}, + {file = "ada_url-1.32.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa1c9b5a41da603dedb1d5bafac6ea4df449f88fea482e7e845494fa250a3a1c"}, + {file = "ada_url-1.32.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:387c8578f38e21fc25eeb791df22644b22321872d4cfc6e9158102cf240c6224"}, + {file = "ada_url-1.32.0-cp313-cp313-win_amd64.whl", hash = "sha256:49ed3336a72ac0193dfe98b9addc231bdac2c052cc752f4c404b4b780afa661a"}, + {file = "ada_url-1.32.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2553ba679830d32480715bc048fb5b5ea94e42006506eb8ad09c12fb2e1d19c8"}, + {file = "ada_url-1.32.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:251a26590edac8063d1b0d9eb4ed81ca47ad9c0d613146f337443f66bc07c5c3"}, + {file = "ada_url-1.32.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2a7f637309414fa9355f714a2c1268427b84fdb1cb3bdf2f76e6f59723651019"}, + {file = "ada_url-1.32.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fea5031dbad3b46fd93e105ca869ca5b0e630b6d2c4a929eb92f3508cfd6b0d5"}, + {file = "ada_url-1.32.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8b5b5541e6f6f4eefdfdf823e2892d15f173c638312d6ca0f2243fb38c70230"}, + {file = "ada_url-1.32.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:314534a62f63fefd53f43696780260f8b23326004aa641d71ef74a33c6b064bd"}, + {file = "ada_url-1.32.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a9acfd240f9c20c1ed2052ad9a8412f30ad6b03ed4a9e965fb56b871c2fdf7b5"}, + {file = "ada_url-1.32.0-cp314-cp314-win_amd64.whl", hash = "sha256:eea67edc7c7dcb784d7e43149c5d4bfc7f394af8a1cbfc8a14e6ec317b4bf8e4"}, + {file = "ada_url-1.32.0.tar.gz", hash = "sha256:ceefeb2e89d6e007ed58caa461e802b7a0f39fbbbdd7e39a4511fc1e3a6f9777"}, +] + +[package.dependencies] +cffi = "*" + +[[package]] +name = "annotated-types" +version = "0.8.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, +] +markers = {main = "extra == \"mcp\""} + +[[package]] +name = "anyio" +version = "4.15.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] +markers = {main = "extra == \"mcp\""} + +[[package]] +name = "auth0-api-python" +version = "1.0.0b10" +description = "SDK for verifying access tokens and securing APIs with Auth0, using Authlib." +optional = false +python-versions = ">=3.9.2" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +ada-url = {version = "^1.30.0", markers = "python_version >= \"3.10\""} +authlib = "^1.0" +cryptography = ">=43.0.1" +httpx = "^0.28.1" +jwcrypto = "^1.5.7" +requests = "^2.31.0" + +[package.source] +type = "git" +url = "https://github.com/auth0/auth0-api-python.git" +reference = "1fe1a417e867d417b4bd44c5984fe002f11149f9" +resolved_reference = "1fe1a417e867d417b4bd44c5984fe002f11149f9" + +[[package]] +name = "authlib" +version = "1.9.1" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "authlib-1.9.1-py2.py3-none-any.whl", hash = "sha256:8b9be8b1e5174dcbf5520e984f1a22dc2edae5b6e40a61d4cedbf86b97e9e69b"}, + {file = "authlib-1.9.1.tar.gz", hash = "sha256:5c7d9848f47cac340f060f76ae0bc09b59f5c8e89cc1bb032d4370a0c55b5974"}, +] + +[package.dependencies] +cryptography = ">=45.0.1" +joserfc = ">=1.6.8" + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + +[[package]] +name = "certifi" +version = "2026.10.19" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b"}, + {file = "certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae"}, +] + +[[package]] +name = "cffi" +version = "2.1.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, +] +markers = {dev = "platform_python_implementation != \"PyPy\""} + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, + {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, + {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, +] + +[[package]] +name = "click" +version = "8.5.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, +] +markers = {main = "extra == \"mcp\" and sys_platform != \"emscripten\"", dev = "sys_platform != \"emscripten\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "50.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.9" +groups = ["main", "dev"] +files = [ + {file = "cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b"}, + {file = "cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648"}, + {file = "cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149"}, + {file = "cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf"}, + {file = "cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e"}, + {file = "cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b"}, + {file = "cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +ssh = ["bcrypt (>=3.1.5)"] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] +markers = {dev = "sys_platform != \"emscripten\""} + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpcore2" +version = "2.13.0" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e"}, + {file = "httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db"}, +] +markers = {main = "extra == \"mcp\" and sys_platform != \"emscripten\"", dev = "sys_platform != \"emscripten\""} + +[package.dependencies] +h11 = ">=0.16" +truststore = ">=0.10" + +[package.extras] +asyncio = ["anyio (>=4.5.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.34.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "httpx2" +version = "2.13.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a"}, + {file = "httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +anyio = {version = ">=4.10", markers = "sys_platform != \"emscripten\""} +httpcore2 = {version = "2.13.0", markers = "sys_platform != \"emscripten\""} +httpx2-jsfetch = {version = "*", markers = "sys_platform == \"emscripten\" and python_version >= \"3.12\""} +idna = ">=3.18" +truststore = {version = ">=0.10", markers = "sys_platform != \"emscripten\""} +typing-extensions = {version = ">=4.5.0", markers = "python_version < \"3.13\""} + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.2) ; platform_python_implementation != \"CPython\""] +cli = ["click (>=8.4.2)", "pygments (==2.*)", "rich (>=10,<16)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +ws = ["wsproto (>=1.2)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version <= \"3.13\""] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +description = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +optional = false +python-versions = ">=3.12" +groups = ["main", "dev"] +files = [ + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, +] +markers = {main = "sys_platform == \"emscripten\" and python_version >= \"3.12\" and extra == \"mcp\"", dev = "sys_platform == \"emscripten\" and python_version >= \"3.12\""} + +[[package]] +name = "idna" +version = "3.19" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, + {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, +] + +[package.extras] +all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "joserfc" +version = "1.7.5" +description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159"}, + {file = "joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea"}, +] + +[package.dependencies] +cryptography = ">=45.0.1" + +[package.extras] +drafts = ["pycryptodome"] + +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.3.6" +referencing = ">=0.28.4" +rpds-py = ">=0.25.0" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jwcrypto" +version = "1.6.1" +description = "Implementation of JOSE Web standards" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jwcrypto-1.6.1-py3-none-any.whl", hash = "sha256:77e856818e514cb3d64a1043b6885a7d52f5cc37773f5e944de02103c8d3a54f"}, + {file = "jwcrypto-1.6.1.tar.gz", hash = "sha256:a1e1570da5c2e35dbcd375ec1d2891a24de8eb35ee2c1fe8e91978efb72cfe90"}, +] + +[package.dependencies] +cryptography = ">=49.0.0" +typing_extensions = ">=4.5.0" + +[[package]] +name = "mcp" +version = "2.3.0" +description = "Model Context Protocol SDK" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "mcp-2.3.0-py3-none-any.whl", hash = "sha256:dd0c44c089d16453e8ae31a3877a0054d7a2314caaa81f5e0541b9b1734b2377"}, + {file = "mcp-2.3.0.tar.gz", hash = "sha256:8b147a50441cf059dc88c684e0aeed3687f0aa0f39c6cde7b90330effd2b34d8"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +anyio = [ + {version = ">=4.9", markers = "python_version < \"3.14\""}, + {version = ">=4.10", markers = "python_version >= \"3.14\""}, +] +httpx2 = ">=2.10.0" +jsonschema = ">=4.20.0" +mcp-types = "2.3.0" +opentelemetry-api = ">=1.28.0" +pydantic = ">=2.12.0" +pyjwt = {version = ">=2.10.1", extras = ["crypto"]} +python-multipart = ">=0.0.9" +pywin32 = {version = ">=311", markers = "sys_platform == \"win32\""} +sse-starlette = ">=3.0.0" +starlette = [ + {version = ">=0.27", markers = "python_version < \"3.14\""}, + {version = ">=0.48.0", markers = "python_version >= \"3.14\""}, +] +typing-extensions = ">=4.13.0" +typing-inspection = ">=0.4.1" +uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""} + +[package.extras] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"] +rich = ["rich (>=13.9.4)"] + +[[package]] +name = "mcp-types" +version = "2.3.0" +description = "Model Context Protocol wire types" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "mcp_types-2.3.0-py3-none-any.whl", hash = "sha256:968efdbdaedfab06adae40d378a34395f1090c5921d4be3c9cde283aaf76d91d"}, + {file = "mcp_types-2.3.0.tar.gz", hash = "sha256:d1e46549edb35ee19a94940fcee6d1addd7e589ab7ea92dda83f5d84781fc362"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +pydantic = ">=2.12.0" +typing-extensions = ">=4.13.0" + +[[package]] +name = "opentelemetry-api" +version = "1.45.2" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "opentelemetry_api-1.45.2-py3-none-any.whl", hash = "sha256:2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34"}, + {file = "opentelemetry_api-1.45.2.tar.gz", hash = "sha256:d7b0008e21e5fd61cbe7a7f34ca3b6905a78ab677d9bc395e299359c75cda6b4"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +typing-extensions = ">=4.5.0" + +[[package]] +name = "packaging" +version = "26.3" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] +markers = {main = "implementation_name != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} + +[[package]] +name = "pydantic" +version = "2.13.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygments" +version = "2.21.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyjwt" +version = "2.15.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193"}, + {file = "pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] + +[[package]] +name = "pytest" +version = "9.2.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588"}, + {file = "pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=2" +packaging = ">=24" +pluggy = ">=1.6,<2" +pygments = ">=2.15" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "asynctest ; python_version < \"3.11\"", "attrs (>=23.1)", "coverage (>=7.5)", "hypothesis (>=6.75)", "mock", "numpy (>=1.26)", "pexpect (>=4.9)", "pytest-xdist (>=3.5)", "pyyaml", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.4.1" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:507afa78ec77c6661164ef1a2368bc7820383fd1a14b2a7f813de8b5a92949cc"}, + {file = "pytest_asyncio-1.4.1.tar.gz", hash = "sha256:e8266eb094e5ee9ffdb2068c9836f33e209ea77a970eb846933d416b5602669b"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dotenv" +version = "1.2.4" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc"}, + {file = "python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.32" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, + {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, +] +markers = {main = "extra == \"mcp\""} + +[[package]] +name = "pywin32" +version = "312" +description = "Python for Windows Extensions" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, + {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, + {file = "pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd"}, + {file = "pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c"}, + {file = "pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a"}, + {file = "pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47"}, + {file = "pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b"}, + {file = "pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc"}, + {file = "pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950"}, + {file = "pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c"}, + {file = "pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9"}, + {file = "pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831"}, + {file = "pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b"}, + {file = "pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e"}, + {file = "pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa"}, + {file = "pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed"}, + {file = "pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5"}, + {file = "pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9"}, + {file = "pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5"}, + {file = "pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb"}, + {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, +] +markers = {main = "extra == \"mcp\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "requests" +version = "2.34.2" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "rpds-py" +version = "0.30.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, +] +markers = {main = "python_version < \"3.14\" and extra == \"mcp\"", dev = "python_version < \"3.14\""} + +[[package]] +name = "rpds-py" +version = "2026.6.3" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.11" +groups = ["main", "dev"] +files = [ + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, + {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, +] +markers = {main = "python_version >= \"3.14\" and extra == \"mcp\"", dev = "python_version >= \"3.14\""} + +[[package]] +name = "ruff" +version = "0.17.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4"}, + {file = "ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11"}, + {file = "ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e"}, + {file = "ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2"}, + {file = "ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe"}, + {file = "ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca"}, + {file = "ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24"}, + {file = "ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9"}, + {file = "ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9"}, +] + +[[package]] +name = "sse-starlette" +version = "3.4.11" +description = "SSE plugin for Starlette" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453"}, + {file = "sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +anyio = ">=4.7.0" +starlette = ">=0.49.1" + +[package.extras] +daphne = ["daphne (>=4.2.0)"] +examples = ["fastapi (>=0.115.12)", "pydantic (>=2)", "uvicorn (>=0.34.0)"] +examples-db = ["aiosqlite (>=0.21.0)", "sqlalchemy[asyncio] (>=2.0.41)"] +granian = ["granian (>=2.3.1)"] +uvicorn = ["uvicorn (>=0.34.0)"] + +[[package]] +name = "starlette" +version = "1.7.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +markers = "python_version < \"3.14\"" +files = [ + {file = "starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e"}, + {file = "starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d"}, +] + +[package.dependencies] +anyio = ">=4.0.0,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "starlette" +version = "1.9.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.11" +groups = ["main", "dev"] +markers = "python_version >= \"3.14\"" +files = [ + {file = "starlette-1.9.0-py3-none-any.whl", hash = "sha256:fd3f159936e882d06c1108f84688f7cda2cf85fc6a592a50edbd175e882c2ddc"}, + {file = "starlette-1.9.0.tar.gz", hash = "sha256:3d931de246ed870e35ce605974440816ef895e592f255d1396cfdf191d4c4318"}, +] + +[package.dependencies] +anyio = ">=4.0.0,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "truststore" +version = "0.10.4" +description = "Verify certificates using native system trust stores" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, + {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, +] +markers = {main = "extra == \"mcp\" and sys_platform != \"emscripten\"", dev = "sys_platform != \"emscripten\""} + +[[package]] +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, +] +markers = {main = "extra == \"mcp\""} + +[package.dependencies] +typing-extensions = ">=4.15.0" + +[[package]] +name = "urllib3" +version = "2.8.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "uvicorn" +version = "0.52.4" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1"}, + {file = "uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86"}, +] +markers = {main = "extra == \"mcp\" and sys_platform != \"emscripten\"", dev = "sys_platform != \"emscripten\""} + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=13.0)"] + +[extras] +mcp = ["mcp"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10,<4.0" +content-hash = "407e01acf42d671cc427d9991f583eb64dce046c5a6bcb55b2d83f7be1ed45cf" diff --git a/packages/auth0-ai-mcp/pyproject.toml b/packages/auth0-ai-mcp/pyproject.toml new file mode 100644 index 0000000..1380f7d --- /dev/null +++ b/packages/auth0-ai-mcp/pyproject.toml @@ -0,0 +1,32 @@ +[tool.poetry] +name = "auth0-ai-mcp" +version = "1.0.0b0" +description = "Auth0 authentication helpers for MCP servers, built on auth0-api-python." +authors = ["Auth0 "] +license = "Apache-2.0" +readme = "README.md" +packages = [{ include = "auth0_ai_mcp", from = "src" }] + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" +# Pinned by SHA until a core release includes the OBO, M2M and Token Vault caching, the malformed-token fix and client_info. +auth0-api-python = { git = "https://github.com/auth0/auth0-api-python.git", rev = "1fe1a417e867d417b4bd44c5984fe002f11149f9" } +starlette = ">=0.37.0" +mcp = { version = ">=2.2.0", optional = true } + +[tool.poetry.extras] +mcp = ["mcp"] + +[tool.poetry.group.dev.dependencies] +mcp = "^2.2.0" +python-dotenv = "^1.2.3" +pytest = "^9.0" +pytest-asyncio = ">=0.23" +ruff = ">=0.6" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/__init__.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/__init__.py new file mode 100644 index 0000000..9582c5d --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/__init__.py @@ -0,0 +1,88 @@ +"""Auth0 authentication helpers for MCP servers. + +Verifies Auth0 access tokens, builds the caller, enforces scopes, and exchanges tokens +(On-Behalf-Of, client credentials, Token Vault) with an optional `token_store` for caching. +It owns no MCP framework, so each framework gets a thin adapter around it. +""" + +from __future__ import annotations + +from auth0_api_python import ( + AbstractTokenStore, + ApiClient, + ApiClientOptions, + GetAccessTokenForConnectionError, + GetClientCredentialsTokenError, + IndexedTokenStore, + TokenIndexMember, + TokenSet, + VerifiedToken, +) +from auth0_api_python.errors import ( + ApiError, + AuthServerUnavailableError, + BaseAuthError, + ConfigurationError, + DomainsResolverError, + GetTokenByExchangeProfileError, + MissingOrganizationError, + MissingRequiredArgumentError, + OrganizationNotAllowedError, + VerifyAccessTokenError, +) + +from .client import McpServerClient, create_mcp_client +from .errors import ( + DownscopedTokenError, + InsufficientScopeError, + UnauthenticatedError, +) +from .principal import Principal, build_principal +from .request_context import ( + RequestContext, + bind_request_context, + get_caller_mcp_client, + get_caller_principal, + require_caller_mcp_client, + require_caller_principal, +) +from .request_scoped_client import RequestScopedMcpClient +from .scopes import require_scopes, with_required_scopes + +__all__ = [ + "AbstractTokenStore", + "ApiClient", + "ApiClientOptions", + "ApiError", + "AuthServerUnavailableError", + "BaseAuthError", + "ConfigurationError", + "DomainsResolverError", + "DownscopedTokenError", + "GetAccessTokenForConnectionError", + "GetClientCredentialsTokenError", + "GetTokenByExchangeProfileError", + "IndexedTokenStore", + "InsufficientScopeError", + "McpServerClient", + "MissingOrganizationError", + "MissingRequiredArgumentError", + "OrganizationNotAllowedError", + "Principal", + "RequestContext", + "RequestScopedMcpClient", + "TokenIndexMember", + "TokenSet", + "UnauthenticatedError", + "VerifiedToken", + "VerifyAccessTokenError", + "bind_request_context", + "build_principal", + "create_mcp_client", + "get_caller_mcp_client", + "get_caller_principal", + "require_caller_mcp_client", + "require_caller_principal", + "require_scopes", + "with_required_scopes", +] diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/client.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/client.py new file mode 100644 index 0000000..f67e366 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/client.py @@ -0,0 +1,197 @@ +"""The server client: verify incoming tokens and exchange them for downstream tokens. + +This sits beside auth0-api-python. It owns no MCP framework and registers no tools. +It verifies the bearer token, delegates the OBO exchange to the core SDK (which handles +caching when a token_store is wired in via create_mcp_client), and hands tool code a +request-scoped client. Each framework adapter wires this into a request pipeline. +""" + +from __future__ import annotations + +from typing import Literal + +from auth0_api_python import ( + AbstractTokenStore, + ApiClient, + ApiClientOptions, + GetAccessTokenForConnectionError, + GetClientCredentialsTokenError, + TokenSet, + VerifiedToken, +) +from auth0_api_python.errors import ( + ConfigurationError, + GetTokenByExchangeProfileError, + VerifyAccessTokenError, +) + +from .principal import Principal +from .request_scoped_client import RequestScopedMcpClient +from .utils._internal import exchange, telemetry + + +class McpServerClient: + """Verifies incoming tokens and exchanges them for downstream API tokens.""" + + def __init__( + self, + api_client: ApiClient, + ): + self._api = api_client + + @property + def domain(self) -> str | None: + """The tenant domain the core client was configured with, or None in multi-domain mode.""" + return self._api.options.domain + + @property + def audience(self) -> str: + """The API identifier the core client expects in incoming tokens.""" + return self._api.options.audience + + def with_token(self, verified: VerifiedToken, principal: Principal) -> RequestScopedMcpClient: + """Bind this client to one request's verified token and principal.""" + return RequestScopedMcpClient(self, verified, principal) + + async def verify_access_token(self, access_token: str) -> VerifiedToken: + """Verify the incoming token, returning it paired with its verified claims. + + Raises: + VerifyAccessTokenError: If the token is invalid, or is bound to a DPoP key or a client certificate (it has a `cnf` claim) and so cannot be used as a Bearer token. + AuthServerUnavailableError: If the OIDC discovery document or JWKS cannot be reached. + """ + claims = await self._api.verify_access_token(access_token=access_token) + if claims.get("cnf"): + raise VerifyAccessTokenError("Sender-constrained token cannot be used as a Bearer token") + return VerifiedToken(access_token=access_token, claims=claims) + + async def get_token_on_behalf_of( + self, + verified: VerifiedToken, + *, + audience: str, + scope: str | None = None, + ) -> TokenSet: + """Exchange the caller's token for a downstream token for the given audience and scope. + + Raises: + ApiError: If the token endpoint is unreachable, times out, or returns an error. + DownscopedTokenError: If the exchange grants fewer scopes than were requested. + GetTokenByExchangeProfileError: If client credentials are not configured or the exchanged token is already expired. + """ + result = await self._api.get_token_on_behalf_of( + verified.access_token, audience, scope, verified=verified + ) + exchange.reject_if_downscoped(scope, result) + exchange.reject_if_expired(result, GetTokenByExchangeProfileError) + return exchange.token_set(result) + + async def get_client_credentials_token( + self, *, audience: str, scope: str | None = None + ) -> TokenSet: + """Obtain a client credentials token for a server-to-server call. + + Args: + audience: Target API identifier. + scope: Optional space-separated scopes to request. + + Raises: + GetClientCredentialsTokenError: If client credentials are not configured or the returned token is already expired. + ApiError: If the token endpoint is unreachable, times out, or returns an error. + DownscopedTokenError: If the token endpoint grants fewer scopes than were requested. + """ + result = await self._api.get_client_credentials_token(audience=audience, scope=scope) + exchange.reject_if_downscoped(scope, result) + exchange.reject_if_expired(result, GetClientCredentialsTokenError) + return exchange.token_set(result) + + async def get_token_for_connection( + self, + verified: VerifiedToken, + *, + connection: str, + login_hint: str | None = None, + ) -> TokenSet: + """Exchange the caller's token for a federated provider token via Token Vault. + + Args: + verified: The verified incoming token for the requesting user. + connection: The Auth0 federated connection identifier (e.g. "google-oauth2"). + login_hint: Optional value that selects which linked account to use when the user has several linked accounts on this connection. + + Raises: + GetAccessTokenForConnectionError: If client credentials are not configured or the returned token is already expired. + ApiError: If the token exchange endpoint is unreachable, times out, or returns an error. + """ + options: dict = { + "connection": connection, + "access_token": verified.access_token, + **({"login_hint": login_hint} if login_hint else {}), + } + result = await self._api.get_access_token_for_connection(options, verified=verified) + exchange.reject_if_expired(result, GetAccessTokenForConnectionError) + return exchange.token_set(result) + + +def create_mcp_client( + domain: str | None = None, + audience: str | None = None, + *, + client_id: str | None = None, + client_secret: str | None = None, + token_store: AbstractTokenStore | None = None, + scope_matching: Literal["strict", "non_strict"] = "strict", + organization_policy: Literal["required", "allow"] = "allow", + organization_id: str | list[str] | None = None, + client_info: dict | None = None, + api_client: ApiClient | None = None, +) -> McpServerClient: + """Build an McpServerClient. + + Args: + domain: The Auth0 tenant domain, for example "tenant.auth0.com". Required unless `api_client` is passed. + audience: The API identifier the incoming tokens are issued for. Required unless `api_client` is passed. + client_id: Needed for the on-behalf-of exchange, client credentials tokens and Token Vault. Token verification works without it. + client_secret: Needed alongside `client_id` for the same flows. + token_store: Optional store for the core SDK's token cache. + scope_matching: Cache scope-matching mode, "strict" or "non_strict". "non_strict" needs an `IndexedTokenStore`. + organization_policy: "required" rejects tokens without an `org_id` claim, "allow" accepts them. + organization_id: The Organization ID or IDs a token must carry. Only valid with `organization_policy="required"`. + client_info: Identifies a package built on this one in the Auth0-Client header. A dict with non-empty string "name" and "version" keys and an optional "env" dict of string keys and values. + api_client: A prebuilt core client to use as is. Pass it instead of the options above, including `domain` and `audience`. + + Returns: + The server client. + + Raises: + ConfigurationError: If `domain` or `audience` is missing without `api_client`, or if `api_client` is passed together with any of `domain`, `audience`, `client_id`, `client_secret`, `token_store`, `scope_matching`, `organization_policy`, `organization_id` or `client_info`, or if `client_info` does not have the shape described above. + """ + if api_client is None: + if domain is None or audience is None: + raise ConfigurationError("domain and audience are required unless api_client is passed") + api_client = ApiClient( + ApiClientOptions( + domain=domain, + audience=audience, + client_id=client_id, + client_secret=client_secret, + token_store=token_store, + scope_matching=scope_matching, + organization_policy=organization_policy, + organization_id=organization_id, + client_info=telemetry.build_client_info(client_info), + ) + ) + elif ( + domain is not None + or audience is not None + or client_id is not None + or client_secret is not None + or token_store is not None + or scope_matching != "strict" + or organization_policy != "allow" + or organization_id is not None + or client_info is not None + ): + raise ConfigurationError("api_client cannot be combined with other client options, because they would be ignored") + return McpServerClient(api_client) diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/errors.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/errors.py new file mode 100644 index 0000000..d610c56 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/errors.py @@ -0,0 +1,47 @@ +"""Errors owned by auth0-ai-mcp. + +The core SDK (auth0-api-python) owns errors for the functions it provides, and this +package raises those directly. These are the errors with no core equivalent. They all +extend BaseAuthError so a caller can catch one base type across both layers. +""" + +from __future__ import annotations + +from auth0_api_python.errors import BaseAuthError + +try: + from mcp.server.mcpserver.exceptions import ToolError +except ImportError: # Without mcp there is no framework error to extend. + _TOOL_ERROR_BASES: tuple[type[Exception], ...] = () +else: + _TOOL_ERROR_BASES = (ToolError,) + + +class UnauthenticatedError(BaseAuthError, *_TOOL_ERROR_BASES): + """No verified caller in the current request context. Raised inside the tool, so the framework reports it as a tool error.""" + + def get_status_code(self) -> int: + return 401 + + def get_error_code(self) -> str: + return "invalid_token" + + +class InsufficientScopeError(BaseAuthError, *_TOOL_ERROR_BASES): + """The caller's token is missing a required scope. Raised inside the tool, so the framework reports it as a tool error.""" + + def get_status_code(self) -> int: + return 403 + + def get_error_code(self) -> str: + return "insufficient_scope" + + +class DownscopedTokenError(BaseAuthError): + """The exchange granted fewer scopes than were requested. Error code `downscoped_token`, status 403.""" + + def get_status_code(self) -> int: + return 403 + + def get_error_code(self) -> str: + return "downscoped_token" diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/middleware.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/middleware.py new file mode 100644 index 0000000..841850a --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/middleware.py @@ -0,0 +1,243 @@ +"""ASGI middleware: wires Auth0 verification into any Starlette/FastAPI or official MCP app. + +The middleware verifies the incoming bearer token, builds the caller's Principal, and sets +the request context before the MCP handler runs, so tool code reaches the caller through +`get_caller_principal()` / `require_caller_mcp_client()`. A missing or invalid token +gets a 401 with a `WWW-Authenticate` challenge and never reaches a tool. If Auth0 cannot be reached +to verify the token, the response is a 503 without a challenge. It also serves the RFC 9728 +`/.well-known/oauth-protected-resource` metadata document, at the root and at the path derived +from the resource URL. WebSocket connections are refused. +""" + +from __future__ import annotations + +import json +import logging +from urllib.parse import urlsplit + +from auth0_api_python.errors import ( + AuthServerUnavailableError, + DomainsResolverError, + VerifyAccessTokenError, +) +from starlette.authentication import AuthCredentials +from starlette.types import ASGIApp, Receive, Scope, Send + +from .client import McpServerClient +from .principal import build_principal +from .request_context import RequestContext, bind_request_context +from .utils._internal import headers, mcp_session, scope_input, urls + +logger = logging.getLogger(__name__) + +_PRM_PATH = "/.well-known/oauth-protected-resource" +_INVALID_DESCRIPTION = "The access token is not valid" + + +def build_protected_resource_metadata( + mcp_client: McpServerClient, + *, + resource_url: str | None = None, + authorization_servers: list[str] | None = None, + scopes_supported: list[str] | None = None, + resource_name: str | None = None, + resource_documentation: str | None = None, +) -> dict: + """Build the RFC 9728 protected-resource metadata document. + + Args: + mcp_client: The server client. Its audience is the default `resource_url` and its domain the default authorization server. + resource_url: Absolute http or https URL of the protected resource. Defaults to the client audience and must equal it when given. + authorization_servers: Issuer URLs to advertise, each an absolute https URL. Defaults to the client's tenant domain and is required when the client has no single domain. + scopes_supported: Optional scopes to advertise. Omitted from the document when None. + resource_name: Optional human-readable name for the resource. Omitted from the document when None. + resource_documentation: Optional URL of documentation for the resource. Omitted from the document when None. + + Returns: + The metadata document as a dict. + + Raises: + ConfigurationError: If `resource_url` differs from the client audience or is not an absolute http or https URL, or if `authorization_servers` cannot be resolved or holds anything but absolute https URLs, or if `scopes_supported` is not a list of single-value strings. + """ + doc: dict = { + "resource": urls.resolve_resource_url(mcp_client, resource_url), + "authorization_servers": urls.resolve_authorization_servers(mcp_client, authorization_servers), + "bearer_methods_supported": ["header"], + } + if scopes_supported is not None: + doc["scopes_supported"] = scope_input.normalize_advertised_scopes(scopes_supported) + if resource_name is not None: + doc["resource_name"] = resource_name + if resource_documentation is not None: + doc["resource_documentation"] = resource_documentation + return doc + + +def build_www_authenticate( + resource_metadata_url: str, + error: str | None = None, + error_description: str | None = None, + scope: str | None = None, +) -> str: + """Build a Bearer `WWW-Authenticate` challenge with every value made safe for a quoted header. + + Args: + resource_metadata_url: URL of the protected-resource metadata document. + error: Optional RFC 6750 error code. Leave None when the request carried no token. + error_description: Optional human-readable description, only sent alongside `error`. + scope: Optional space-separated scopes the client should request. Omitted from the challenge when None. + + Returns: + The challenge, ASCII only, for example `Bearer resource_metadata="https://host/.well-known/oauth-protected-resource"`. + """ + params = [] + if error is not None: + params.append(f'error="{headers.header_safe(error)}"') + if error_description is not None: + params.append(f'error_description="{headers.header_safe(error_description)}"') + params.append(f'resource_metadata="{headers.header_safe(resource_metadata_url)}"') + if scope is not None: + params.append(f'scope="{headers.header_safe(scope)}"') + return "Bearer " + ", ".join(params) + + +class Auth0AsgiMiddleware: + """Verifies the bearer token and populates the request context for each request. + + Args: + app: The ASGI app to protect. + mcp_client: The server client that verifies tokens. + resource_url: Absolute http or https URL of the protected resource. Defaults to the client audience and must equal it when given. + authorization_servers: Issuer URLs to advertise, each an absolute https URL. Defaults to the client's tenant domain and is required when the client has no single domain. + scopes_supported: Optional scopes to advertise in the metadata document and in the `401` challenge. + resource_name: Optional human-readable name for the resource, advertised in the metadata document. + resource_documentation: Optional URL of documentation for the resource, advertised in the metadata document. + + Raises: + ConfigurationError: If `resource_url` differs from the client audience or is not an absolute http or https URL, or if `authorization_servers` cannot be resolved or holds anything but absolute https URLs, or if `scopes_supported` is not a list of single-value strings. + """ + + def __init__( + self, + app: ASGIApp, + *, + mcp_client: McpServerClient, + resource_url: str | None = None, + authorization_servers: list[str] | None = None, + scopes_supported: list[str] | None = None, + resource_name: str | None = None, + resource_documentation: str | None = None, + ): + self._app = app + self._client = mcp_client + self._metadata = build_protected_resource_metadata( + mcp_client, + resource_url=resource_url, + authorization_servers=authorization_servers, + scopes_supported=scopes_supported, + resource_name=resource_name, + resource_documentation=resource_documentation, + ) + parts = urlsplit(self._metadata["resource"]) + suffixed_path = _PRM_PATH + urls.resource_path(parts.path) + self._metadata_paths = {_PRM_PATH, suffixed_path} + self._metadata_url = f"{parts.scheme}://{parts.netloc}{suffixed_path}" + self._challenge_scope = " ".join(self._metadata.get("scopes_supported", [])) or None + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] == "websocket": + await send({"type": "websocket.close", "code": 1008}) + return + if scope["type"] != "http": + await self._app(scope, receive, send) + return + + method = scope.get("method") + if method == "GET" and scope["path"] in self._metadata_paths: + await self._send_json(send, 200, self._metadata) + return + + # Browsers send a CORS preflight without credentials, so it cannot be verified. + if method == "OPTIONS": + await self._app(scope, receive, send) + return + + token = headers.bearer_token(scope) + if token is None: + await self._send_unauthorized(send) + return + + if not headers.is_well_formed_token(token): + logger.warning("Token rejected: malformed bearer value") + await self._send_unauthorized(send, error="invalid_token") + return + + try: + verified = await self._client.verify_access_token(token) + principal = build_principal(verified.claims) + except AuthServerUnavailableError as exc: + logger.error("Auth0 could not be reached to verify the token: %r", str(exc)) + await self._send_unavailable(send) + return + except DomainsResolverError as exc: + logger.warning("The domains resolver failed while verifying the token: %r", str(exc)) + await self._send_unavailable(send) + return + except VerifyAccessTokenError as exc: + logger.warning("Token rejected: %r", str(exc)) + await self._send_unauthorized(send, error="invalid_token") + return + + context = RequestContext( + principal=principal, + mcp_client=self._client.with_token(verified, principal), + ) + if mcp_session.AuthenticatedUser is not None: + scope["user"] = mcp_session.AuthenticatedUser(mcp_session.access_token_info(principal, verified.claims)) + scope["auth"] = AuthCredentials(principal.scopes) + with bind_request_context(context): + await self._app(scope, receive, send) + + async def _send_json( + self, send: Send, status: int, body: dict, headers: list[tuple[bytes, bytes]] | None = None + ) -> None: + raw = json.dumps(body).encode() + response_headers = [(b"content-type", b"application/json")] + if headers: + response_headers.extend(headers) + await send( + {"type": "http.response.start", "status": status, "headers": response_headers} + ) + await send({"type": "http.response.body", "body": raw}) + + async def _send_unauthorized(self, send: Send, error: str | None = None) -> None: + if error is None: + challenge = build_www_authenticate(self._metadata_url, scope=self._challenge_scope) + message = "Unauthorized" + else: + challenge = build_www_authenticate( + self._metadata_url, error, _INVALID_DESCRIPTION, scope=self._challenge_scope + ) + message = f"Unauthorized: {_INVALID_DESCRIPTION}" + payload = { + "jsonrpc": "2.0", + "error": {"code": -32001, "message": message}, + "id": None, + } + await self._send_json( + send, + 401, + payload, + headers=[(b"www-authenticate", challenge.encode("ascii")), (b"cache-control", b"no-store")], + ) + + async def _send_unavailable(self, send: Send) -> None: + # No challenge, so the client keeps its token and retries instead of logging in again. + payload = { + "jsonrpc": "2.0", + "error": {"code": -32003, "message": "Service unavailable: the token could not be verified"}, + "id": None, + } + await self._send_json( + send, 503, payload, headers=[(b"retry-after", b"5"), (b"cache-control", b"no-store")] + ) diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/principal.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/principal.py new file mode 100644 index 0000000..1cc0c49 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/principal.py @@ -0,0 +1,89 @@ +""" +Verified caller identity, normalized from access token claims. +""" + +import math +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + +from auth0_api_python.errors import VerifyAccessTokenError + + +@dataclass +class Principal: + """ + Normalized caller identity built from a verified access token's claims. + + Attributes: + sub: The subject claim identifying the caller. + expires_at: Unix epoch seconds the token expires, from the "exp" claim. + scopes: Parsed from the space-separated "scope" claim. Empty list if absent. + permissions: From the "permissions" claim. None when the claim is absent from + the token (RBAC is off, or the API does not add permissions to the access token), a list (possibly empty) when present. + client_id: From the "azp" claim, falling back to "client_id". Always set, because a token with neither is rejected. + org_id: From the "org_id" claim. None when the token carries no Organization. + """ + + sub: str + expires_at: int | float + scopes: list[str] + permissions: list[str] | None + client_id: str + org_id: str | None + + +def build_principal(claims: Mapping[str, Any]) -> Principal: + """ + Build a Principal from a verified access token's claims. + + Args: + claims: The claims dict returned by ApiClient.verify_access_token. This must be + the already-verified claims, never a raw token or unverified input. + + Returns: + A Principal normalizing the claims tool code needs. + + Raises: + VerifyAccessTokenError: If "sub" is missing or blank, "exp" is missing or not a finite number, neither "azp" nor "client_id" is a non-blank string, "permissions" is not a list of strings, or "org_id" is not a string. + """ + sub = claims.get("sub") + if not isinstance(sub, str) or not sub.strip(): + raise VerifyAccessTokenError("Access token is missing the required 'sub' claim") + + exp = claims.get("exp") + if isinstance(exp, bool) or not isinstance(exp, (int, float)) or not math.isfinite(exp): + raise VerifyAccessTokenError("Access token has a missing or invalid 'exp' claim") + + client_id = next( + ( + value + for value in (claims.get("azp"), claims.get("client_id")) + if isinstance(value, str) and value.strip() + ), + None, + ) + if client_id is None: + raise VerifyAccessTokenError("Access token is missing the 'azp' or 'client_id' claim") + + permissions = claims.get("permissions") + if permissions is not None and ( + not isinstance(permissions, list) or not all(isinstance(p, str) for p in permissions) + ): + raise VerifyAccessTokenError("Access token has an invalid 'permissions' claim") + + org_id = claims.get("org_id") + if org_id is not None and not isinstance(org_id, str): + raise VerifyAccessTokenError("Access token has an invalid 'org_id' claim") + + scope = claims.get("scope") or "" + scopes = scope.split() if isinstance(scope, str) else [] + + return Principal( + sub=sub, + expires_at=exp, + scopes=scopes, + permissions=permissions, + client_id=client_id, + org_id=org_id, + ) diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/py.typed b/packages/auth0-ai-mcp/src/auth0_ai_mcp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/request_context.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/request_context.py new file mode 100644 index 0000000..8576db3 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/request_context.py @@ -0,0 +1,94 @@ +"""Per-request state, reachable from tool code without threading arguments. + +The adapter verifies the token and sets this contextvar before the MCP handler runs. +Tool code reads the caller and its request-scoped client from here, so a tool signature +stays clean and never has to accept an auth object it did not ask for. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +from contextvars import ContextVar, Token +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from .errors import UnauthenticatedError +from .principal import Principal + +if TYPE_CHECKING: + from .request_scoped_client import RequestScopedMcpClient + + +@dataclass +class RequestContext: + """ + The per-request state that tool code reads. + + Attributes: + principal: The verified caller for this request. + mcp_client: The request-scoped client bound to that caller's token. + """ + + principal: Principal + mcp_client: RequestScopedMcpClient + + +_request_context: ContextVar[RequestContext | None] = ContextVar( + "auth0_ai_mcp_request_context", default=None +) + + +def set_request_context(context: RequestContext) -> Token[RequestContext | None]: + """Set the current request context, returning the token to reset it after.""" + return _request_context.set(context) + + +def reset_request_context(token: Token[RequestContext | None]) -> None: + """Reset the request context to its prior state.""" + _request_context.reset(token) + + +@contextmanager +def bind_request_context(context: RequestContext) -> Iterator[None]: + """Set the request context for the duration of a `with` block. + + Args: + context: The verified caller and request-scoped client for this request. + + Yields: + None. The context is reset on exit, including when the block raises. + """ + token = set_request_context(context) + try: + yield + finally: + reset_request_context(token) + + +def get_caller_principal() -> Principal | None: + """Return the verified caller for this request, or None outside a request.""" + ctx = _request_context.get() + return ctx.principal if ctx else None + + +def require_caller_principal() -> Principal: + """Return the verified caller, raising if called outside a request.""" + principal = get_caller_principal() + if principal is None: + raise UnauthenticatedError("No principal in the current request context.") + return principal + + +def get_caller_mcp_client() -> RequestScopedMcpClient | None: + """Return the request-scoped client, or None outside a request.""" + ctx = _request_context.get() + return ctx.mcp_client if ctx else None + + +def require_caller_mcp_client() -> RequestScopedMcpClient: + """Return the request-scoped client, raising if called outside a request.""" + client = get_caller_mcp_client() + if client is None: + raise UnauthenticatedError("No MCP client in the current request context.") + return client diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/request_scoped_client.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/request_scoped_client.py new file mode 100644 index 0000000..14f6b33 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/request_scoped_client.py @@ -0,0 +1,50 @@ +"""A client bound to one request's token. + +Tool code calls `get_token_on_behalf_of` on this without ever handling the raw +incoming token. The token is closed over here, so a tool cannot accidentally forward +or leak it. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .principal import Principal + +if TYPE_CHECKING: + from auth0_api_python import TokenSet, VerifiedToken + + from .client import McpServerClient + + +class RequestScopedMcpClient: + """Wraps the shared server client with this request's verified token and principal.""" + + def __init__(self, client: McpServerClient, verified: VerifiedToken, principal: Principal): + self._client = client + self._verified = verified + self._principal = principal + + async def get_token_on_behalf_of(self, *, audience: str, scope: str | None = None) -> TokenSet: + """Exchange the caller's token for a downstream token for `audience`.""" + return await self._client.get_token_on_behalf_of( + self._verified, audience=audience, scope=scope + ) + + async def get_client_credentials_token( + self, *, audience: str, scope: str | None = None + ) -> TokenSet: + """Obtain a client credentials token for a server-to-server call.""" + return await self._client.get_client_credentials_token(audience=audience, scope=scope) + + async def get_token_for_connection( + self, *, connection: str, login_hint: str | None = None + ) -> TokenSet: + """Exchange the caller's token for a federated provider token via Token Vault.""" + return await self._client.get_token_for_connection( + self._verified, connection=connection, login_hint=login_hint + ) + + def get_principal(self) -> Principal: + """Return the verified caller for this request.""" + return self._principal diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/scopes.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/scopes.py new file mode 100644 index 0000000..513af1e --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/scopes.py @@ -0,0 +1,77 @@ +"""Scope-gating decorator for async MCP tool handlers. + +`with_required_scopes` checks the incoming MCP token's scopes before the tool body +runs and raises if the caller is missing any of the requested scopes. It reads the +verified caller from the request context the adapter set up, so the tool body does +not receive an auth argument. +""" + +from __future__ import annotations + +import functools +import inspect +import logging +from collections.abc import Awaitable, Callable +from typing import TypeVar + +from auth0_api_python.errors import ConfigurationError + +from .errors import InsufficientScopeError, UnauthenticatedError +from .principal import Principal +from .request_context import get_caller_principal +from .utils._internal import scope_input + +logger = logging.getLogger(__name__) + +F = TypeVar("F", bound=Callable[..., Awaitable[object]]) + + +def require_scopes(principal: Principal | None, scopes: str | list[str]) -> None: + """Raise if the principal is missing any of the given scopes. + + Args: + principal: The verified caller, or None when there is no request context. + scopes: A single required scope, or a list of scopes that must all be present. + + Raises: + ConfigurationError: If `scopes` is empty, blank, or holds a non-string, blank or whitespace-containing entry. + UnauthenticatedError: If `principal` is None. + InsufficientScopeError: If the principal is missing any required scope. + """ + required = scope_input.normalize_scopes(scopes) + if principal is None: + raise UnauthenticatedError("No principal in the current request context.") + logger.debug("Token scopes: %s, required: %s", principal.scopes, required) + if any(s not in principal.scopes for s in required): + logger.info("Scope check failed. The token is missing one or more required scopes.") + raise InsufficientScopeError("Missing one or more required scopes.") + + +def with_required_scopes(scopes: str | list[str]) -> Callable[[F], F]: + """Return a decorator that gates an async handler on the given scopes. + + Put the framework's own registration decorator, such as `@mcp.tool()`, above this one. Registered the other way round, the framework keeps the unguarded function and the check is skipped. + + Args: + scopes: A single required scope, or a list of scopes that must all be present. + + Returns: + A decorator that runs the scope check before the handler body. + + Raises: + ConfigurationError: At decoration time, if `scopes` is empty, blank, or holds a non-string, blank or whitespace-containing entry, or if the decorated function is not async. + """ + required = scope_input.normalize_scopes(scopes) + + def decorator(func: F) -> F: + if not inspect.iscoroutinefunction(func): + raise ConfigurationError("with_required_scopes can only gate an async function") + + @functools.wraps(func) + async def wrapper(*args: object, **kwargs: object) -> object: + require_scopes(get_caller_principal(), required) + return await func(*args, **kwargs) + + return wrapper # type: ignore[return-value] + + return decorator diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/__init__.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/__init__.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/__init__.py new file mode 100644 index 0000000..fcae5f0 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/__init__.py @@ -0,0 +1 @@ +"""Private helpers. Not part of the public API.""" diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/exchange.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/exchange.py new file mode 100644 index 0000000..9b710a0 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/exchange.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import time + +from auth0_api_python import TokenSet +from auth0_api_python.errors import BaseAuthError + +from ...errors import DownscopedTokenError + + +def token_set(result: dict) -> TokenSet: + """Build the same `TokenSet` shape for every flow, adding `granted_scopes` when the response has a scope.""" + tokens: TokenSet = { + "access_token": result["access_token"], + "expires_at": result["expires_at"], + } + if result.get("scope"): + tokens["granted_scopes"] = result["scope"] + return tokens + + +def reject_if_expired(result: dict, error: type[BaseAuthError]) -> None: + """Raise `error` if the returned token has no usable `expires_at` or has already expired, with a separate message for each.""" + expires_at = result.get("expires_at") + if isinstance(expires_at, bool) or not isinstance(expires_at, (int, float)): + raise error("The exchanged token has no usable expiry.") + if expires_at <= time.time(): + raise error("The exchanged token is already expired.") + + +def reject_if_downscoped(requested: str | None, result: dict) -> None: + """Raise if the exchange granted fewer scopes than were requested. + + An absent `scope` response field means the grant matched the request (RFC 6749 + section 5.1), so only a present field is diffed. The message stays generic on + purpose and does not name the missing scopes. + """ + if not requested or "scope" not in result: + return + granted = {s for s in (result["scope"] or "").split() if s} + missing = {s for s in requested.split() if s} - granted + if missing: + raise DownscopedTokenError( + "Exchanged token did not include all requested scopes." + ) diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/headers.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/headers.py new file mode 100644 index 0000000..7007e11 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/headers.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from starlette.types import Scope + + +def header_safe(value: str) -> str: + """Replace anything that could end or escape a quoted header value, or is not printable ASCII.""" + quoted = value.replace('"', "'") + return "".join(ch if " " <= ch <= "~" and ch != "\\" else " " for ch in quoted) + + +def bearer_token(scope: Scope) -> str | None: + """Pull the bearer token from the Authorization header, or None if absent or blank.""" + for name, value in scope.get("headers", []): + if name == b"authorization": + parts = value.decode("latin-1").split(" ", 1) + if len(parts) == 2 and parts[0].lower() == "bearer": + return parts[1].strip() or None + return None + + +def is_well_formed_token(token: str) -> bool: + """Whether the token has no whitespace inside it, so malformed values never reach verification.""" + return not any(ch.isspace() for ch in token) diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/mcp_session.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/mcp_session.py new file mode 100644 index 0000000..e41a937 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/mcp_session.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ...principal import Principal + +try: + from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser + from mcp.server.auth.provider import AccessToken +except ModuleNotFoundError: # Only a missing mcp is tolerated, so a renamed class fails loudly. + AuthenticatedUser = None + AccessToken = None + +# Tool code can reach scope["user"], so the raw bearer token never goes there. +_REDACTED_VALUE = "redacted" + + +def access_token_info(principal: Principal, claims: Mapping[str, Any]) -> AccessToken: + """Describe the caller to the mcp session manager, which keys a session on client, issuer and subject.""" + issuer = claims.get("iss") + return AccessToken( + token=_REDACTED_VALUE, + client_id=principal.client_id, + scopes=principal.scopes, + expires_at=int(principal.expires_at), + subject=principal.sub, + claims={"iss": issuer} if isinstance(issuer, str) else None, + ) diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/scope_input.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/scope_input.py new file mode 100644 index 0000000..a22cd39 --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/scope_input.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from auth0_api_python.errors import ConfigurationError + + +def normalize_scopes(scopes: str | list[str]) -> list[str]: + """Return the required scopes as a list, rejecting an empty or malformed argument. + + Args: + scopes: A single scope string or a list of scope strings. + + Returns: + The required scopes as a list. + + Raises: + ConfigurationError: If `scopes` is empty, blank, not a string or list, or holds a non-string, blank or whitespace-containing entry. + """ + required = [scopes] if isinstance(scopes, str) else scopes + if not isinstance(required, list) or not required: + raise ConfigurationError("scopes must be a non-empty string or a non-empty list of strings") + if any(not isinstance(s, str) or not s.strip() for s in required): + raise ConfigurationError("scopes must not contain blank or non-string entries") + if any(any(c.isspace() for c in s) for s in required): + raise ConfigurationError("each scope must be a single value without whitespace, so pass a list for several scopes") + return list(required) + + +def normalize_advertised_scopes(scopes: list[str]) -> list[str]: + """Return the scopes to advertise, where an empty list means none are advertised. + + Args: + scopes: The scopes to advertise. + + Returns: + The scopes as a list. + + Raises: + ConfigurationError: If `scopes` is not a list, or holds a non-string, blank or whitespace-containing entry. + """ + if not isinstance(scopes, list): + raise ConfigurationError("scopes_supported must be a list of strings") + return normalize_scopes(scopes) if scopes else [] diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/telemetry.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/telemetry.py new file mode 100644 index 0000000..2d7128a --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/telemetry.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import importlib.metadata + +_PACKAGE_NAME = "auth0-ai-mcp" + + +def _package_version() -> str: + try: + return importlib.metadata.version(_PACKAGE_NAME) + except importlib.metadata.PackageNotFoundError: + return "unknown" + + +def build_client_info(wrapper: dict | None = None) -> dict: + """Build the client_info passed to the core SDK, reporting this package or the wrapper built on it.""" + if wrapper is None: + return {"name": _PACKAGE_NAME, "version": _package_version()} + if not isinstance(wrapper, dict) or not isinstance(wrapper.get("env", {}), dict): + return wrapper + return {**wrapper, "env": {**wrapper.get("env", {}), _PACKAGE_NAME: _package_version()}} diff --git a/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/urls.py b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/urls.py new file mode 100644 index 0000000..ea1eb5e --- /dev/null +++ b/packages/auth0-ai-mcp/src/auth0_ai_mcp/utils/_internal/urls.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from urllib.parse import urlsplit + +from auth0_api_python.errors import ConfigurationError +from auth0_api_python.utils import normalize_domain + +if TYPE_CHECKING: + from ...client import McpServerClient + + +def resolve_resource_url(mcp_client: McpServerClient, resource_url: str | None) -> str: + """Return the advertised resource URL, defaulting to the client audience, which tokens must carry as `aud`.""" + audience = mcp_client.audience + if resource_url is None: + resource_url = audience + elif resource_url != audience: + raise ConfigurationError( + "resource_url must equal the client audience, because tokens for any other audience are rejected. " + "Omit resource_url to use the audience." + ) + if not is_plain_url(resource_url, ("http", "https")): + raise ConfigurationError( + "resource_url (the client audience unless set) must be an absolute http or https URL with a host and no fragment, credentials or whitespace" + ) + return resource_url + + +def resolve_authorization_servers( + mcp_client: McpServerClient, authorization_servers: list[str] | None +) -> list[str]: + """Return the advertised issuers, defaulting to the client's tenant domain.""" + if authorization_servers is None: + domain = mcp_client.domain + if not domain: + raise ConfigurationError( + "authorization_servers is required when the client has no single domain" + ) + try: + issuer = normalize_domain(domain) + except ValueError as exc: + raise ConfigurationError(f"domain is not a valid tenant domain: {exc}") from exc + # The core builds its discovery URL from the raw domain, so anything normalize_domain trims would break verification. + if issuer != f"https://{domain.lower()}/" or any(ch.isspace() for ch in issuer): + raise ConfigurationError("domain must be a bare host such as tenant.auth0.com, with no scheme, path or whitespace") + return [issuer] + if not isinstance(authorization_servers, list) or not authorization_servers: + raise ConfigurationError("authorization_servers must be a non-empty list of https URLs") + for server in authorization_servers: + if not is_plain_url(server, ("https",)) or urlsplit(server).query: + raise ConfigurationError( + "authorization_servers must hold absolute https URLs with a host and no query, fragment, credentials or whitespace" + ) + return authorization_servers + + +def is_plain_url(value: object, schemes: tuple[str, ...]) -> bool: + """True for an absolute URL of one of `schemes` with a host and no fragment, credentials or whitespace.""" + if not isinstance(value, str) or any(ch.isspace() or not ch.isprintable() for ch in value): + return False + parts = urlsplit(value) + return parts.scheme in schemes and bool(parts.hostname) and not parts.fragment and parts.username is None and parts.password is None + + +def resource_path(path: str) -> str: + """The resource URL's path for RFC 9728 path insertion, empty when the path is only "/".""" + return "" if path == "/" else path diff --git a/packages/auth0-ai-mcp/tests/test_client.py b/packages/auth0-ai-mcp/tests/test_client.py new file mode 100644 index 0000000..b7a3da4 --- /dev/null +++ b/packages/auth0-ai-mcp/tests/test_client.py @@ -0,0 +1,578 @@ +"""OBO exchange and downscope-detection behavior.""" + +import base64 +import importlib.metadata +import json +import time + +import pytest +from auth0_api_python import ApiClient, ApiClientOptions, VerifiedToken +from auth0_api_python.errors import ( + ApiError, + ConfigurationError, + GetTokenByExchangeProfileError, + VerifyAccessTokenError, +) + +from auth0_ai_mcp import ( + AbstractTokenStore, + DownscopedTokenError, + GetAccessTokenForConnectionError, + GetClientCredentialsTokenError, + IndexedTokenStore, + McpServerClient, + Principal, + RequestScopedMcpClient, + create_mcp_client, +) + + +def _jwt(claims: dict) -> str: + """Build an unsigned JWT carrying the given claims.""" + payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).rstrip(b"=").decode() + return f"header.{payload}.sig" + + +def _verified(sub: str = "user_1", org_id: str | None = None) -> VerifiedToken: + """A verified token and its claims for OBO calls.""" + claims = {"sub": sub, "iss": "https://tenant.example/"} + if org_id is not None: + claims["org_id"] = org_id + return VerifiedToken(access_token=_jwt(claims), claims=claims) + + +_KEEP = object() +_MISSING = object() + + +class StubApiClient: + """Stands in for auth0-api-python's ApiClient, recording exchange calls.""" + + def __init__( + self, + *, + granted_scope=None, + include_scope=True, + expires_at=None, + raises=None, + cc_scope_response=None, + expires_at_override=_KEEP, + claims=None, + ): + self.claims = claims + self.granted_scope = granted_scope + self.include_scope = include_scope + self.expires_at = expires_at if expires_at is not None else int(time.time()) + 3600 + self.expires_at_override = expires_at_override + self.raises = raises + self.exchange_calls = 0 + self.last_verified = None + self.last_access_token = None + self.last_scope = None + self.cc_calls = 0 + self.cc_last_audience = None + self.cc_last_scope = None + self.cc_scope_response = cc_scope_response + self.connection_calls = 0 + self.last_connection_options = None + + async def verify_access_token(self, access_token: str): + if self.claims is not None: + return self.claims + return {"sub": "user_1", "exp": int(time.time()) + 3600, "scope": "mcp:use"} + + async def get_token_on_behalf_of(self, access_token, audience, scope=None, *, verified=None): + self.exchange_calls += 1 + self.last_verified = verified + self.last_access_token = access_token + self.last_scope = scope + if self.raises is not None: + raise self.raises + granted = self.granted_scope if self.granted_scope is not None else (scope or "") + result = { + "access_token": f"downstream-for-{audience}", + "expires_in": self.expires_at - int(time.time()), + "expires_at": self.expires_at, + } + if self.include_scope: + result["scope"] = granted + return self._with_expiry(result) + + def _with_expiry(self, result): + if self.expires_at_override is _MISSING: + result.pop("expires_at") + elif self.expires_at_override is not _KEEP: + result["expires_at"] = self.expires_at_override + return result + + async def get_client_credentials_token(self, audience, scope=None): + self.cc_calls += 1 + self.cc_last_audience = audience + self.cc_last_scope = scope + if self.raises is not None: + raise self.raises + result = {"access_token": f"m2m-for-{audience}", "expires_in": 3600, "expires_at": self.expires_at} + if self.cc_scope_response is not None: + result["scope"] = self.cc_scope_response + return self._with_expiry(result) + + async def get_access_token_for_connection(self, options, *, verified=None): + self.connection_calls += 1 + self.last_connection_options = options + self.last_verified = verified + if self.raises is not None: + raise self.raises + connection = options.get("connection", "") + return self._with_expiry( + {"access_token": f"vault-for-{connection}", "expires_in": 3600, "expires_at": self.expires_at} + ) + + +async def test_exchange_returns_downstream_token(): + api = StubApiClient() + client = McpServerClient(api) + + result = await client.get_token_on_behalf_of( + _verified(), audience="https://orders/", scope="cancel:orders" + ) + + assert result == { + "access_token": "downstream-for-https://orders/", + "expires_at": api.expires_at, + "granted_scopes": "cancel:orders", + } + assert api.exchange_calls == 1 + + +_ERROR_BY_FLOW = { + "obo": GetTokenByExchangeProfileError, + "client_credentials": GetClientCredentialsTokenError, + "token_vault": GetAccessTokenForConnectionError, +} + + +async def _run_flow(client: McpServerClient, flow: str, scope: str | None = "cancel:orders"): + if flow == "obo": + return await client.get_token_on_behalf_of(_verified(), audience="https://orders/", scope=scope) + if flow == "client_credentials": + return await client.get_client_credentials_token(audience="https://orders/", scope=scope) + return await client.get_token_for_connection(_verified(), connection="google-oauth2") + + +@pytest.mark.parametrize("flow", ["obo", "client_credentials"]) +async def test_absent_scope_in_response_means_granted_matched_and_is_not_downscoped(flow): + """RFC 6749: an omitted scope field means the grant matched the request.""" + client = McpServerClient(StubApiClient(include_scope=False)) + + result = await _run_flow(client, flow, scope="read:orders write:orders") + + assert "granted_scopes" not in result + + +async def test_verify_access_token_returns_verified_token_carrying_the_original_access_token(): + client = McpServerClient(StubApiClient()) + + verified = await client.verify_access_token("the.raw.token") + + assert verified.access_token == "the.raw.token" + assert verified.claims["sub"] == "user_1" + + +@pytest.mark.parametrize("cnf", [{"jkt": "thumbprint"}, {"x5t#S256": "thumbprint"}, {"jkt": ""}, "not-a-dict"]) +async def test_verify_access_token_rejects_a_sender_constrained_token(cnf): + client = McpServerClient(StubApiClient(claims={"sub": "user_1", "cnf": cnf})) + + with pytest.raises(VerifyAccessTokenError): + await client.verify_access_token("the.raw.token") + + +@pytest.mark.parametrize("cnf", [{}, None]) +async def test_verify_access_token_accepts_an_empty_cnf_claim(cnf): + client = McpServerClient(StubApiClient(claims={"sub": "user_1", "cnf": cnf})) + + verified = await client.verify_access_token("the.raw.token") + + assert verified.claims["sub"] == "user_1" + + +async def test_exchange_forwards_verified_token_to_core(): + """The verified token is forwarded so core exchanges its access token and skips re-verifying.""" + api = StubApiClient() + client = McpServerClient(api) + verified = _verified() + + await client.get_token_on_behalf_of( + verified, audience="https://orders/", scope="cancel:orders" + ) + + assert api.last_verified is verified + assert api.last_verified.claims["sub"] == "user_1" + + +async def test_downscoped_exchange_raises_downscoped_token_error(): + """A shortfall between requested and granted scopes raises the MCP-owned error.""" + api = StubApiClient(granted_scope="read:orders") + client = McpServerClient(api) + + with pytest.raises(DownscopedTokenError): + await client.get_token_on_behalf_of( + _verified(), audience="https://orders/", scope="cancel:orders" + ) + + +def test_downscoped_error_has_distinct_error_code(): + error = DownscopedTokenError("x") + assert error.get_error_code() == "downscoped_token" + assert error.get_status_code() == 403 + + +async def test_downscoped_error_message_does_not_leak_scopes(): + """The message stays generic and never embeds the granted or missing scope values.""" + api = StubApiClient(granted_scope="read:orders") + client = McpServerClient(api) + + with pytest.raises(DownscopedTokenError) as exc_info: + await client.get_token_on_behalf_of( + _verified(), audience="https://orders/", scope="cancel:orders" + ) + + message = str(exc_info.value) + assert "cancel:orders" not in message + assert "read:orders" not in message + + +@pytest.mark.parametrize("flow", ["obo", "client_credentials"]) +async def test_empty_scope_in_response_is_downscoped(flow): + """A present but empty scope field means nothing was granted, unlike an absent one.""" + client = McpServerClient(StubApiClient(granted_scope="", cc_scope_response="")) + + with pytest.raises(DownscopedTokenError): + await _run_flow(client, flow) + + +async def test_no_requested_scope_skips_downscope_check(): + api = StubApiClient(granted_scope="read:orders") + client = McpServerClient(api) + + result = await client.get_token_on_behalf_of( + _verified(), audience="https://orders/", scope=None + ) + + assert result["access_token"] == "downstream-for-https://orders/" + + +async def test_exchange_rejection_propagates_core_error(): + """A core ApiError from the exchange propagates unchanged. The MCP layer does not wrap it.""" + api = StubApiClient(raises=ApiError("access_denied", "rejected by Auth0", 403)) + client = McpServerClient(api) + + with pytest.raises(ApiError): + await client.get_token_on_behalf_of( + _verified(), audience="https://orders/", scope="read:orders" + ) + + +@pytest.mark.parametrize("flow", ["obo", "client_credentials", "token_vault"]) +@pytest.mark.parametrize( + ("expires_at", "message"), + [ + (int(time.time()) - 1, "already expired"), + (_MISSING, "no usable expiry"), + (None, "no usable expiry"), + ("soon", "no usable expiry"), + ], + ids=["past", "missing", "none", "non_numeric"], +) +async def test_result_without_a_future_numeric_expires_at_is_rejected(flow, expires_at, message): + client = McpServerClient(StubApiClient(expires_at_override=expires_at)) + + with pytest.raises(_ERROR_BY_FLOW[flow], match=message): + await _run_flow(client, flow) + + +async def test_get_client_credentials_token_returns_token_set(): + api = StubApiClient() + client = McpServerClient(api) + + result = await client.get_client_credentials_token(audience="https://payments/", scope="process:payments") + + assert result["access_token"] == "m2m-for-https://payments/" + assert api.cc_calls == 1 + assert api.cc_last_audience == "https://payments/" + assert api.cc_last_scope == "process:payments" + + +async def test_get_client_credentials_token_raises_when_downscoped(): + api = StubApiClient(cc_scope_response="read:payments") + client = McpServerClient(api) + + with pytest.raises(DownscopedTokenError): + await client.get_client_credentials_token( + audience="https://payments/", scope="process:payments read:payments" + ) + + +async def test_get_client_credentials_token_passes_when_granted_is_a_reordered_superset(): + api = StubApiClient(cc_scope_response="read:payments process:payments extra:scope") + client = McpServerClient(api) + + result = await client.get_client_credentials_token( + audience="https://payments/", scope="process:payments read:payments" + ) + + assert result["granted_scopes"] == "read:payments process:payments extra:scope" + + +async def test_get_client_credentials_token_propagates_error(): + api = StubApiClient(raises=GetClientCredentialsTokenError("no credentials")) + client = McpServerClient(api) + + with pytest.raises(GetClientCredentialsTokenError): + await client.get_client_credentials_token(audience="https://payments/") + + +async def test_get_token_for_connection_returns_token_set(): + api = StubApiClient() + client = McpServerClient(api) + verified = _verified() + + result = await client.get_token_for_connection(verified, connection="google-oauth2") + + assert result["access_token"] == "vault-for-google-oauth2" + assert api.connection_calls == 1 + assert api.last_connection_options == {"connection": "google-oauth2", "access_token": verified.access_token} + assert api.last_verified is verified + + +async def test_get_token_for_connection_includes_login_hint_when_provided(): + api = StubApiClient() + client = McpServerClient(api) + verified = _verified() + + await client.get_token_for_connection(verified, connection="google-oauth2", login_hint="user@example.com") + + assert api.last_connection_options == { + "connection": "google-oauth2", + "access_token": verified.access_token, + "login_hint": "user@example.com", + } + + +async def test_get_token_for_connection_skips_downscope_check(): + """Token Vault scopes are set on the connection, so a granted scope field is not compared.""" + api = StubApiClient() + + async def connection_with_scope(options, *, verified=None): + return {"access_token": "vault", "expires_in": 3600, "expires_at": api.expires_at, "scope": "other"} + + api.get_access_token_for_connection = connection_with_scope + client = McpServerClient(api) + + result = await client.get_token_for_connection(_verified(), connection="google-oauth2") + + assert result["access_token"] == "vault" + assert result["granted_scopes"] == "other" + + +async def test_get_token_for_connection_propagates_error(): + api = StubApiClient(raises=GetAccessTokenForConnectionError("exchange failed")) + client = McpServerClient(api) + + with pytest.raises(GetAccessTokenForConnectionError): + await client.get_token_for_connection(_verified(), connection="google-oauth2") + + +def _principal() -> Principal: + return Principal(sub="user_1", expires_at=9999999999, scopes=[], permissions=None, client_id="client_1", org_id=None) + + +async def test_request_scoped_get_token_on_behalf_of_forwards_scope_and_verified_token(): + api = StubApiClient() + verified = _verified() + scoped = RequestScopedMcpClient(McpServerClient(api), verified, _principal()) + + result = await scoped.get_token_on_behalf_of(audience="https://orders/", scope="cancel:orders") + + assert result["access_token"] == "downstream-for-https://orders/" + assert api.last_scope == "cancel:orders" + assert api.last_access_token == verified.access_token + assert api.last_verified is verified + + +async def test_request_scoped_get_client_credentials_token(): + api = StubApiClient() + server_client = McpServerClient(api) + scoped = RequestScopedMcpClient(server_client, _verified(), _principal()) + + result = await scoped.get_client_credentials_token(audience="https://payments/", scope="process:payments") + + assert result["access_token"] == "m2m-for-https://payments/" + assert api.cc_last_audience == "https://payments/" + assert api.cc_last_scope == "process:payments" + + +async def test_request_scoped_get_token_for_connection_injects_verified(): + """The request-scoped client injects self._verified. The caller does not supply it.""" + api = StubApiClient() + verified = _verified() + server_client = McpServerClient(api) + scoped = RequestScopedMcpClient(server_client, verified, _principal()) + + result = await scoped.get_token_for_connection(connection="google-oauth2", login_hint="user@example.com") + + assert result["access_token"] == "vault-for-google-oauth2" + assert api.last_verified is verified + assert api.last_connection_options["login_hint"] == "user@example.com" + + +@pytest.mark.parametrize( + "option", + [ + {"domain": "tenant.example"}, + {"audience": "https://api/"}, + {"client_id": "id"}, + {"client_secret": "secret"}, + {"token_store": object()}, + {"scope_matching": "non_strict"}, + {"organization_policy": "required"}, + {"organization_id": "org_1"}, + {"client_info": {"name": "wrapper", "version": "1.0.0"}}, + ], +) +def test_create_mcp_client_rejects_options_passed_with_api_client(option): + with pytest.raises(ConfigurationError): + create_mcp_client(api_client=StubApiClient(), **option) + + +@pytest.mark.parametrize("args", [{}, {"domain": "tenant.example"}, {"audience": "https://api/"}]) +def test_create_mcp_client_needs_domain_and_audience_without_api_client(args): + with pytest.raises(ConfigurationError, match="domain and audience"): + create_mcp_client(**args) + + +def test_client_domain_is_none_in_multi_domain_mode(): + client = create_mcp_client( + api_client=ApiClient(ApiClientOptions(domains=["a.example", "b.example"], audience="https://api/")) + ) + + assert client.domain is None + assert client.audience == "https://api/" + + +def test_create_mcp_client_forwards_org_policy_to_core(): + client = create_mcp_client( + "tenant.auth0.com", + "https://api/", + organization_policy="required", + organization_id="org_abc", + ) + assert client.audience == "https://api/" + assert client._api.options.organization_policy == "required" + assert client._api.options.organization_id == "org_abc" + + +def test_create_mcp_client_forwards_client_credentials_to_core(): + client = create_mcp_client( + "tenant.auth0.com", "https://api/", client_id="cid", client_secret="csecret" + ) + assert client._api.options.client_id == "cid" + assert client._api.options.client_secret == "csecret" + + +def test_create_mcp_client_passes_token_store_to_api_client_options(): + """create_mcp_client wires token_store into ApiClientOptions so core's cache is active.""" + + class _MinimalStore(AbstractTokenStore): + async def get(self, key): + return None + + async def set(self, key, value): + pass + + async def delete(self, key): + pass + + store = _MinimalStore(secret="s" * 32) + client = create_mcp_client("tenant.auth0.com", "https://api/", token_store=store) + assert client._api.options.token_store is store + + +def test_create_mcp_client_forwards_scope_matching_to_core(): + class _MinimalIndexedStore(IndexedTokenStore): + async def get(self, key): + return None + + async def set(self, key, value): + pass + + async def delete(self, key): + pass + + async def add_index_member(self, index_key, member): + pass + + async def list_index_members(self, index_key): + return [] + + client = create_mcp_client( + "tenant.auth0.com", + "https://api/", + token_store=_MinimalIndexedStore(secret="s" * 32), + scope_matching="non_strict", + ) + assert client._api.options.scope_matching == "non_strict" + + +def test_create_mcp_client_reports_this_package_to_core(): + client = create_mcp_client("tenant.auth0.com", "https://api/") + + assert client._api.options.client_info == { + "name": "auth0-ai-mcp", + "version": importlib.metadata.version("auth0-ai-mcp"), + } + + +def test_create_mcp_client_reports_the_wrapper_and_lists_this_package_in_env(): + client = create_mcp_client( + "tenant.auth0.com", + "https://api/", + client_info={"name": "auth0-ai-fastmcp", "version": "1.2.3", "env": {"fastmcp": "3.0"}}, + ) + + assert client._api.options.client_info == { + "name": "auth0-ai-fastmcp", + "version": "1.2.3", + "env": {"fastmcp": "3.0", "auth0-ai-mcp": importlib.metadata.version("auth0-ai-mcp")}, + } + + +def test_create_mcp_client_wrapper_cannot_replace_this_packages_env_entry(): + client = create_mcp_client( + "tenant.auth0.com", + "https://api/", + client_info={"name": "wrapper", "version": "1.0.0", "env": {"auth0-ai-mcp": "0.0.0"}}, + ) + + assert client._api.options.client_info["env"]["auth0-ai-mcp"] == importlib.metadata.version("auth0-ai-mcp") + + +def test_client_info_version_is_unknown_when_the_package_is_not_installed(monkeypatch): + def missing(name): + raise importlib.metadata.PackageNotFoundError(name) + + monkeypatch.setattr(importlib.metadata, "version", missing) + + assert create_mcp_client("tenant.auth0.com", "https://api/")._api.options.client_info["version"] == "unknown" + + +@pytest.mark.parametrize( + "client_info", + [ + "wrapper", + {"version": "1.0.0"}, + {"name": "wrapper", "version": "1.0.0", "env": "fastmcp"}, + ], +) +def test_create_mcp_client_rejects_malformed_client_info(client_info): + with pytest.raises(ConfigurationError, match="client_info"): + create_mcp_client("tenant.auth0.com", "https://api/", client_info=client_info) diff --git a/packages/auth0-ai-mcp/tests/test_middleware.py b/packages/auth0-ai-mcp/tests/test_middleware.py new file mode 100644 index 0000000..9cba1bc --- /dev/null +++ b/packages/auth0-ai-mcp/tests/test_middleware.py @@ -0,0 +1,899 @@ +"""ASGI middleware behavior: bearer handling, request context, and protected-resource metadata.""" + +import base64 +import importlib +import json +import logging +import sys +from types import SimpleNamespace + +import httpx +import pytest +from auth0_api_python.errors import ( + AuthServerUnavailableError, + ConfigurationError, + DomainsResolverError, + VerifyAccessTokenError, +) +from mcp.server.auth.middleware import bearer_auth +from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser +from mcp.server.lowlevel import Server +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from starlette.authentication import AuthCredentials + +from auth0_ai_mcp import ( + McpServerClient, + create_mcp_client, + get_caller_mcp_client, + get_caller_principal, +) +from auth0_ai_mcp.middleware import ( + Auth0AsgiMiddleware, + build_protected_resource_metadata, + build_www_authenticate, +) +from auth0_ai_mcp.utils._internal import mcp_session + +RESOURCE_URL = "https://mcp.example/mcp" +DOMAIN = "tenant.example" +PRM_URL = "https://mcp.example/.well-known/oauth-protected-resource/mcp" + + +class StubApiClient: + """Stands in for auth0-api-python's ApiClient, returning claims or raising on verify and recording exchanges.""" + + def __init__(self, *, claims=None, raises=None, domain=DOMAIN, audience=RESOURCE_URL, domains=None): + self.claims = claims + self.raises = raises + self.options = SimpleNamespace(domain=domain, audience=audience, domains=domains) + self.verified_tokens = [] + self.exchanged = [] + + async def verify_access_token(self, access_token): + self.verified_tokens.append(access_token) + if self.raises is not None: + raise self.raises + return self.claims + + async def get_token_on_behalf_of(self, access_token, audience, scope, *, verified): + self.exchanged.append((access_token, verified.claims)) + return {"access_token": "downstream", "expires_at": 4102444800} + + +class StubApp: + """Records whether it ran and which caller it saw.""" + + def __init__(self, *, raises=None): + self.raises = raises + self.calls = 0 + self.seen_principal = None + self.seen_client = None + self.seen_scope = None + + async def __call__(self, scope, receive, send): + self.calls += 1 + self.seen_principal = get_caller_principal() + self.seen_client = get_caller_mcp_client() + self.seen_scope = scope + if self.raises is not None: + raise self.raises + + +def _middleware(app, api, **kwargs): + return Auth0AsgiMiddleware( + app, + mcp_client=McpServerClient(api), + **kwargs, + ) + + +def _scope(path="/mcp", headers=None, scope_type="http", method="POST"): + return {"type": scope_type, "path": path, "method": method, "headers": headers or []} + + +def _bearer(token): + return [(b"authorization", f"Bearer {token}".encode())] + + +async def _call(middleware, scope): + sent = [] + + async def receive(): + return {"type": "http.request"} + + async def send(message): + sent.append(message) + + await middleware(scope, receive, send) + return sent + + +def _status(sent): + return sent[0]["status"] + + +def _header(sent, name): + return dict(sent[0]["headers"]).get(name) + + +def _body(sent): + return json.loads(sent[1]["body"]) + + +def _claims(**overrides): + claims = {"sub": "user_1", "exp": 4102444800, "scope": "read:x write:x", "azp": "client_1"} + claims.update(overrides) + return claims + + +async def test_missing_bearer_token_returns_401_and_skips_app(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope()) + + assert _status(sent) == 401 + assert _header(sent, b"www-authenticate") == f'Bearer resource_metadata="{PRM_URL}"'.encode() + assert _body(sent) == { + "jsonrpc": "2.0", + "error": {"code": -32001, "message": "Unauthorized"}, + "id": None, + } + assert app.calls == 0 + assert api.verified_tokens == [] + + +@pytest.mark.parametrize( + ("method", "path"), + [ + ("GET", "/mcp"), + ("DELETE", "/mcp"), + ("GET", "/.well-known/oauth-protected-resource/other"), + ], +) +async def test_missing_bearer_token_returns_401_for_other_methods_and_paths(method, path): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope(path=path, method=method)) + + assert _status(sent) == 401 + assert app.calls == 0 + assert api.verified_tokens == [] + + +async def test_non_bearer_authorization_header_is_treated_as_missing(): + app = StubApp() + + sent = await _call( + _middleware(app, StubApiClient(claims=_claims())), + _scope(headers=[(b"authorization", b"Basic abc123")]), + ) + + assert _status(sent) == 401 + assert app.calls == 0 + + +@pytest.mark.parametrize("value", [b"Bearer ", b"Bearer ", b"bearer \t"]) +async def test_blank_bearer_token_is_treated_as_missing(value): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope(headers=[(b"authorization", value)])) + + assert _status(sent) == 401 + assert _header(sent, b"www-authenticate") == f'Bearer resource_metadata="{PRM_URL}"'.encode() + assert app.calls == 0 + assert api.verified_tokens == [] + + +@pytest.mark.parametrize( + "token", + ["two parts", "tab\tinside", "line\nbreak", "non\xa0breaking"], + ids=["space", "tab", "newline", "nbsp"], +) +async def test_malformed_bearer_value_gets_invalid_token_without_calling_core(token, caplog): + app = StubApp() + api = StubApiClient(claims=_claims()) + + with caplog.at_level(logging.WARNING, logger="auth0_ai_mcp.middleware"): + sent = await _call(_middleware(app, api), _scope(headers=_bearer(token))) + + assert _status(sent) == 401 + assert b'error="invalid_token"' in _header(sent, b"www-authenticate") + assert app.calls == 0 + assert api.verified_tokens == [] + assert token not in caplog.text + + +async def test_bearer_scheme_is_matched_case_insensitively(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + await _call(_middleware(app, api), _scope(headers=[(b"authorization", b"bearer good")])) + + assert app.calls == 1 + assert api.verified_tokens == ["good"] + + +async def test_challenge_uses_root_metadata_url_when_resource_has_no_path(): + middleware = Auth0AsgiMiddleware( + StubApp(), + mcp_client=McpServerClient(StubApiClient(claims=_claims(), audience="https://mcp.example/")), + ) + + sent = await _call(middleware, _scope()) + + assert _header(sent, b"www-authenticate") == ( + b'Bearer resource_metadata="https://mcp.example/.well-known/oauth-protected-resource"' + ) + + +async def test_challenge_keeps_the_port_of_the_resource_url(): + middleware = Auth0AsgiMiddleware( + StubApp(), + mcp_client=McpServerClient(StubApiClient(claims=_claims(), audience="http://localhost:3002/mcp")), + ) + + sent = await _call(middleware, _scope()) + + assert _status(sent) == 401 + assert b'resource_metadata="http://localhost:3002/.well-known/oauth-protected-resource/mcp"' in _header( + sent, b"www-authenticate" + ) + + +async def test_challenge_lists_the_advertised_scopes_when_there_is_no_token(): + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(StubApp(), api, scopes_supported=["read:x", "write:x"]), _scope()) + + assert _header(sent, b"www-authenticate") == ( + f'Bearer resource_metadata="{PRM_URL}", scope="read:x write:x"'.encode() + ) + + +async def test_invalid_token_challenge_lists_the_advertised_scopes(): + api = StubApiClient(raises=VerifyAccessTokenError("bad")) + + sent = await _call(_middleware(StubApp(), api, scopes_supported=["read:x"]), _scope(headers=_bearer("tok"))) + + assert _header(sent, b"www-authenticate") == ( + f'Bearer error="invalid_token", error_description="The access token is not valid", ' + f'resource_metadata="{PRM_URL}", scope="read:x"' + ).encode() + + +async def test_challenge_has_no_scope_when_the_advertised_list_is_empty(): + sent = await _call(_middleware(StubApp(), StubApiClient(claims=_claims()), scopes_supported=[]), _scope()) + + assert _header(sent, b"www-authenticate") == f'Bearer resource_metadata="{PRM_URL}"'.encode() + + +def _unsigned_jwt(header, payload): + def encode(value): + return base64.urlsafe_b64encode(json.dumps(value).encode()).rstrip(b"=").decode() + + return f"{encode(header)}.{encode(payload)}.signature" + + +@pytest.mark.parametrize( + "header, payload, reason", + [ + ([1], {}, "segment is not a JSON object"), + ({"alg": 5}, {}, "'alg' must be a string"), + ({"alg": "RS256", "kid": "k"}, [1], "segment is not a JSON object"), + ], +) +async def test_a_malformed_token_returns_401_for_a_real_client(header, payload, reason, caplog): + app = StubApp() + middleware = Auth0AsgiMiddleware(app, mcp_client=create_mcp_client(DOMAIN, RESOURCE_URL)) + + with caplog.at_level(logging.WARNING, logger="auth0_ai_mcp.middleware"): + sent = await _call(middleware, _scope(headers=_bearer(_unsigned_jwt(header, payload)))) + + assert _status(sent) == 401 + assert b'error="invalid_token"' in _header(sent, b"www-authenticate") + assert reason in caplog.text + assert app.calls == 0 + + +async def test_invalid_token_gets_a_fixed_description_and_the_reason_is_only_logged(caplog): + app = StubApp() + api = StubApiClient(raises=VerifyAccessTokenError('Signature verification failed: detail\r\nInjected: "header"')) + + with caplog.at_level(logging.WARNING, logger="auth0_ai_mcp.middleware"): + sent = await _call(_middleware(app, api), _scope(headers=_bearer("secret-tok"))) + + assert _status(sent) == 401 + assert app.calls == 0 + assert api.verified_tokens == ["secret-tok"] + assert "Injected" not in _header(sent, b"www-authenticate").decode() + assert "Injected" not in json.dumps(_body(sent)) + assert _header(sent, b"www-authenticate") == ( + b'Bearer error="invalid_token", error_description="The access token is not valid", ' + + f'resource_metadata="{PRM_URL}"'.encode() + ) + assert _body(sent)["error"] == { + "code": -32001, + "message": "Unauthorized: The access token is not valid", + } + assert "Signature verification failed: detail" in caplog.text + assert "secret-tok" not in caplog.text + + +async def test_rejection_reason_is_escaped_in_the_log(caplog): + api = StubApiClient(raises=VerifyAccessTokenError("bad\r\nERROR forged record")) + + with caplog.at_level(logging.WARNING, logger="auth0_ai_mcp.middleware"): + sent = await _call(_middleware(StubApp(), api), _scope(headers=_bearer("tok"))) + + assert _status(sent) == 401 + assert "\r" not in caplog.text + assert "\\r\\nERROR forged record" in caplog.text + + +async def test_unreachable_auth_server_returns_503_without_a_challenge_and_skips_app(caplog): + app = StubApp() + api = StubApiClient(raises=AuthServerUnavailableError("Failed to fetch JWKS")) + + with caplog.at_level(logging.ERROR, logger="auth0_ai_mcp.middleware"): + sent = await _call(_middleware(app, api), _scope(headers=_bearer("secret-tok"))) + + assert _status(sent) == 503 + assert _header(sent, b"retry-after") == b"5" + assert _header(sent, b"www-authenticate") is None + assert _body(sent)["error"]["code"] == -32003 + assert "Failed to fetch JWKS" not in json.dumps(_body(sent)) + assert "Failed to fetch JWKS" in caplog.text + assert "secret-tok" not in caplog.text + assert app.calls == 0 + + +@pytest.mark.parametrize("raises", [None, VerifyAccessTokenError("bad"), AuthServerUnavailableError("down")]) +async def test_error_responses_are_not_cacheable(raises): + api = StubApiClient(claims=_claims(), raises=raises) + headers = [] if raises is None else _bearer("tok") + + sent = await _call(_middleware(StubApp(), api), _scope(headers=headers)) + + assert _status(sent) in (401, 503) + assert _header(sent, b"cache-control") == b"no-store" + + +async def test_metadata_document_is_not_marked_no_store(): + sent = await _call( + _middleware(StubApp(), StubApiClient(claims=_claims())), + _scope(path="/.well-known/oauth-protected-resource/mcp", method="GET"), + ) + + assert _status(sent) == 200 + assert _header(sent, b"cache-control") is None + + +async def test_unreachable_auth_server_reason_is_escaped_in_the_log(caplog): + api = StubApiClient(raises=AuthServerUnavailableError("bad\r\nERROR forged record")) + + with caplog.at_level(logging.ERROR, logger="auth0_ai_mcp.middleware"): + sent = await _call(_middleware(StubApp(), api), _scope(headers=_bearer("tok"))) + + assert _status(sent) == 503 + assert "\r" not in caplog.text + assert "\\r\\nERROR forged record" in caplog.text + + +async def test_failing_domains_resolver_returns_503_without_a_challenge_and_skips_app(caplog): + app = StubApp() + api = StubApiClient(raises=DomainsResolverError("resolver\r\nERROR forged record")) + + with caplog.at_level(logging.WARNING, logger="auth0_ai_mcp.middleware"): + sent = await _call(_middleware(app, api), _scope(headers=_bearer("secret-tok"))) + + assert _status(sent) == 503 + assert _header(sent, b"www-authenticate") is None + assert "resolver" not in json.dumps(_body(sent)) + assert "\r" not in caplog.text + assert "\\r\\nERROR forged record" in caplog.text + assert "secret-tok" not in caplog.text + assert app.calls == 0 + + +async def test_dpop_bound_token_returns_401_and_skips_app(): + app = StubApp() + api = StubApiClient(claims={**_claims(), "cnf": {"jkt": "thumbprint"}}) + + sent = await _call(_middleware(app, api), _scope(headers=_bearer("tok"))) + + assert _status(sent) == 401 + assert 'error="invalid_token"' in _header(sent, b"www-authenticate").decode() + assert app.calls == 0 + + +async def test_non_verification_errors_propagate(): + api = StubApiClient(raises=ValueError("misconfigured")) + app = StubApp() + + with pytest.raises(ValueError): + await _call(_middleware(app, api), _scope(headers=_bearer("tok"))) + + assert app.calls == 0 + + +async def test_claims_without_sub_return_401_and_skip_app(): + app = StubApp() + api = StubApiClient(claims=_claims(sub="")) + + sent = await _call(_middleware(app, api), _scope(headers=_bearer("tok"))) + + assert _status(sent) == 401 + assert app.calls == 0 + + +def test_build_www_authenticate_with_error(): + assert build_www_authenticate(PRM_URL, "invalid_token", "Expired") == ( + f'Bearer error="invalid_token", error_description="Expired", resource_metadata="{PRM_URL}"' + ) + + +def test_build_www_authenticate_with_scope(): + assert build_www_authenticate(PRM_URL, scope="read:x write:x") == ( + f'Bearer resource_metadata="{PRM_URL}", scope="read:x write:x"' + ) + + +def test_build_www_authenticate_sanitizes_the_scope(): + challenge = build_www_authenticate(PRM_URL, scope='a"b\\\r\nX: y') + + assert challenge.endswith("scope=\"a'b X: y\"") + + +def test_build_www_authenticate_sanitizes_every_value(): + challenge = build_www_authenticate('https://h/"x\\', 'bad"\r\n', 'say "hi"\\\r\nX: y \u00e9\u2603') + + assert challenge == ( + "Bearer error=\"bad' \", error_description=\"say 'hi' X: y \", " + "resource_metadata=\"https://h/'x \"" + ) + challenge.encode("ascii") + + +async def test_valid_token_runs_app_with_caller_in_context(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope(headers=_bearer("good"))) + + assert sent == [] + assert app.calls == 1 + assert app.seen_principal.sub == "user_1" + assert app.seen_principal.scopes == ["read:x", "write:x"] + assert api.verified_tokens == ["good"] + + +async def test_context_client_is_bound_to_the_verified_token(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + await _call(_middleware(app, api), _scope(headers=_bearer("good"))) + + await app.seen_client.get_token_on_behalf_of(audience="https://orders/") + + assert app.seen_client.get_principal() is app.seen_principal + assert api.exchanged == [("good", _claims())] + + +async def test_valid_token_sets_authenticated_user_without_the_raw_token(): + app = StubApp() + api = StubApiClient(claims=_claims(iss="https://tenant.example/")) + + await _call(_middleware(app, api), _scope(headers=_bearer("raw-bearer-value"))) + + user = app.seen_scope["user"] + auth = app.seen_scope["auth"] + assert isinstance(user, AuthenticatedUser) + assert isinstance(auth, AuthCredentials) + assert user.access_token.client_id == "client_1" + assert user.access_token.subject == "user_1" + assert user.access_token.claims == {"iss": "https://tenant.example/"} + assert auth.scopes == ["read:x", "write:x"] + assert user.access_token.token != "raw-bearer-value" + assert "raw-bearer-value" not in repr(vars(user)) + user.access_token.model_dump_json() + assert "raw-bearer-value" not in repr(vars(auth)) + + +async def test_rejected_token_sets_no_user_on_the_scope(): + scope = _scope(headers=_bearer("bad")) + + await _call(_middleware(StubApp(), StubApiClient(raises=VerifyAccessTokenError("x"))), scope) + + assert "user" not in scope and "auth" not in scope + + +_SESSION_CLAIMS = { + "alice-1": {"sub": "alice", "azp": "c1", "iss": "https://t/", "exp": 4102444800}, + "alice-2": {"sub": "alice", "azp": "c1", "iss": "https://t/", "exp": 4102444900}, + "bob-1": {"sub": "bob", "azp": "c1", "iss": "https://t/", "exp": 4102444800}, +} + + +class StubTokenTableApiClient: + """Maps each bearer value to its own claim set.""" + + options = SimpleNamespace(domain=DOMAIN, audience=RESOURCE_URL, domains=None) + + async def verify_access_token(self, access_token): + return _SESSION_CLAIMS[access_token] + + +async def test_mcp_session_stays_bound_to_the_caller_that_opened_it(): + manager = StreamableHTTPSessionManager(app=Server("test"), json_response=True) + middleware = Auth0AsgiMiddleware( + manager.handle_request, + mcp_client=McpServerClient(StubTokenTableApiClient()), + ) + headers = {"accept": "application/json, text/event-stream", "content-type": "application/json"} + initialize = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": {"name": "test", "version": "1"}, + }, + } + ping = {"jsonrpc": "2.0", "id": 2, "method": "ping"} + + async with manager.run(), httpx.AsyncClient( + transport=httpx.ASGITransport(app=middleware), base_url="http://localhost" + ) as http: + opened = await http.post( + "/mcp", headers={**headers, "authorization": "Bearer alice-1"}, json=initialize + ) + session = {"mcp-session-id": opened.headers["mcp-session-id"], "mcp-protocol-version": "2025-06-18"} + + other = await http.post( + "/mcp", headers={**headers, **session, "authorization": "Bearer bob-1"}, json=ping + ) + same = await http.post( + "/mcp", headers={**headers, **session, "authorization": "Bearer alice-2"}, json=ping + ) + + assert opened.status_code == 200 + assert other.status_code == 404 + assert same.status_code == 200 + + +@pytest.fixture +def reloaded_mcp_session(monkeypatch): + yield lambda: importlib.reload(mcp_session) + monkeypatch.undo() + importlib.reload(mcp_session) + + +def test_session_binding_is_skipped_when_mcp_is_not_installed(monkeypatch, reloaded_mcp_session): + monkeypatch.setitem(sys.modules, "mcp", None) + monkeypatch.setitem(sys.modules, "mcp.server.auth.middleware.bearer_auth", None) + + module = reloaded_mcp_session() + + assert module.AuthenticatedUser is None + assert module.AccessToken is None + + +def test_a_renamed_mcp_class_fails_loudly_instead_of_skipping_session_binding(monkeypatch, reloaded_mcp_session): + monkeypatch.delattr(bearer_auth, "AuthenticatedUser") + + with pytest.raises(ImportError): + reloaded_mcp_session() + + +async def test_context_is_reset_after_request(): + await _call( + _middleware(StubApp(), StubApiClient(claims=_claims())), + _scope(headers=_bearer("good")), + ) + + assert get_caller_principal() is None + + +async def test_context_is_reset_when_app_raises(): + app = StubApp(raises=RuntimeError("handler failed")) + middleware = _middleware(app, StubApiClient(claims=_claims())) + + with pytest.raises(RuntimeError): + await _call(middleware, _scope(headers=_bearer("good"))) + + assert app.calls == 1 + assert get_caller_principal() is None + + +async def test_lifespan_scope_passes_through_without_verification(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), {"type": "lifespan"}) + + assert sent == [] + assert app.calls == 1 + assert api.verified_tokens == [] + + +async def test_websocket_scope_is_closed_without_running_the_app(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope(scope_type="websocket", headers=_bearer("good"))) + + assert sent == [{"type": "websocket.close", "code": 1008}] + assert app.calls == 0 + assert api.verified_tokens == [] + + +async def test_options_preflight_passes_through_without_verification_or_context(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope(method="OPTIONS")) + + assert sent == [] + assert app.calls == 1 + assert app.seen_principal is None + assert api.verified_tokens == [] + + +async def test_metadata_route_is_served_without_a_token(): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call( + _middleware(app, api, scopes_supported=["read:x"]), + _scope(path="/.well-known/oauth-protected-resource", method="GET"), + ) + + assert _status(sent) == 200 + assert _body(sent) == { + "resource": RESOURCE_URL, + "authorization_servers": [f"https://{DOMAIN}/"], + "bearer_methods_supported": ["header"], + "scopes_supported": ["read:x"], + } + assert app.calls == 0 + assert api.verified_tokens == [] + + +def test_metadata_omits_scopes_supported_when_not_given(): + doc = build_protected_resource_metadata(McpServerClient(StubApiClient())) + + assert "scopes_supported" not in doc + + +def test_metadata_advertises_an_empty_scope_list_as_given(): + doc = build_protected_resource_metadata(McpServerClient(StubApiClient()), scopes_supported=[]) + + assert doc["scopes_supported"] == [] + + +@pytest.mark.parametrize( + "scopes", + ["read:x", ("read:x",), ["read:x", ""], ["read:x", " "], ["read:x write:x"], ["read:x", 1], [None]], + ids=["bare_string", "tuple", "blank", "whitespace_only", "inner_space", "non_string", "none_entry"], +) +def test_metadata_rejects_malformed_scopes_supported(scopes): + with pytest.raises(ConfigurationError, match="scope"): + build_protected_resource_metadata(McpServerClient(StubApiClient()), scopes_supported=scopes) + + +def test_middleware_rejects_malformed_scopes_supported_at_startup(): + with pytest.raises(ConfigurationError, match="scope"): + _middleware(StubApp(), StubApiClient(), scopes_supported="read:x") + + +def test_metadata_omits_resource_name_and_documentation_when_not_given(): + doc = build_protected_resource_metadata(McpServerClient(StubApiClient())) + + assert "resource_name" not in doc + assert "resource_documentation" not in doc + assert "jwks_uri" not in doc + + +async def test_metadata_includes_resource_name_and_documentation_when_given(): + sent = await _call( + _middleware( + StubApp(), + StubApiClient(claims=_claims()), + resource_name="Orders MCP", + resource_documentation="https://docs.example/orders", + ), + _scope(path="/.well-known/oauth-protected-resource", method="GET"), + ) + + body = _body(sent) + assert body["resource_name"] == "Orders MCP" + assert body["resource_documentation"] == "https://docs.example/orders" + + +async def test_metadata_is_served_at_the_path_suffixed_route(): + app = StubApp() + + sent = await _call( + _middleware(app, StubApiClient(claims=_claims())), + _scope(path="/.well-known/oauth-protected-resource/mcp", method="GET"), + ) + + assert _status(sent) == 200 + assert _body(sent)["resource"] == RESOURCE_URL + assert app.calls == 0 + + +async def test_metadata_route_keeps_a_trailing_slash_in_the_resource_path(): + middleware = Auth0AsgiMiddleware( + StubApp(), + mcp_client=McpServerClient(StubApiClient(claims=_claims(), audience="https://mcp.example/mcp/")), + ) + + served = await _call(middleware, _scope(path="/.well-known/oauth-protected-resource/mcp/", method="GET")) + challenged = await _call(middleware, _scope()) + + assert _status(served) == 200 + assert _header(challenged, b"www-authenticate") == ( + b'Bearer resource_metadata="https://mcp.example/.well-known/oauth-protected-resource/mcp/"' + ) + + +@pytest.mark.parametrize( + "path", ["/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/mcp"] +) +async def test_metadata_routes_need_a_token_for_methods_other_than_get(path): + app = StubApp() + api = StubApiClient(claims=_claims()) + + sent = await _call(_middleware(app, api), _scope(path=path, method="POST")) + + assert _status(sent) == 401 + assert app.calls == 0 + + +BUILDERS = [ + build_protected_resource_metadata, + lambda client: Auth0AsgiMiddleware(StubApp(), mcp_client=client), +] + + +@pytest.mark.parametrize( + "api_options", + [{"domain": ""}, {"domain": None}, {"domain": None, "domains": ["a.example", "b.example"]}], + ids=["empty", "none", "multi_domain"], +) +@pytest.mark.parametrize("build", BUILDERS) +def test_a_client_without_a_single_domain_needs_explicit_authorization_servers(build, api_options): + with pytest.raises(ConfigurationError, match="authorization_servers is required"): + build(McpServerClient(StubApiClient(**api_options))) + + +@pytest.mark.parametrize( + "domain", + [ + " ", + "tenant.example/path", + "http://tenant.example", + "https://tenant.example/", + "tenant.example/", + " tenant.example", + "tenant.example\n", + "tenant .example", + ], +) +def test_a_client_domain_that_is_not_a_bare_tenant_host_is_rejected(domain): + with pytest.raises(ConfigurationError, match="tenant domain|bare host"): + build_protected_resource_metadata(McpServerClient(StubApiClient(domain=domain))) + + +def test_a_domain_the_core_client_cannot_discover_from_is_rejected_for_a_real_client(): + client = create_mcp_client("https://tenant.example/", RESOURCE_URL) + + with pytest.raises(ConfigurationError, match="bare host"): + build_protected_resource_metadata(client) + + +@pytest.mark.parametrize( + "audience", + [ + "", + "orders-api", + "/mcp", + "mcp.example/mcp", + "ftp://mcp.example/mcp", + "https:///mcp", + "https://mcp.example/mcp#x", + "https://user:pass@mcp.example/mcp", + "https://mcp.example/m cp", + "https://mcp.ex\tample/mcp", + None, + ], +) +def test_an_audience_that_is_not_an_absolute_http_url_is_rejected(audience): + with pytest.raises(ConfigurationError, match="resource_url"): + build_protected_resource_metadata(McpServerClient(StubApiClient(audience=audience))) + + +def test_the_middleware_rejects_an_invalid_audience_too(): + with pytest.raises(ConfigurationError, match="resource_url"): + Auth0AsgiMiddleware(StubApp(), mcp_client=McpServerClient(StubApiClient(audience="orders-api"))) + + +@pytest.mark.parametrize("resource_url", ["https://mcp.example/other", "https://mcp.example/mcp/", "http://mcp.example/mcp"]) +@pytest.mark.parametrize( + "build", + [ + lambda client, url: build_protected_resource_metadata(client, resource_url=url), + lambda client, url: Auth0AsgiMiddleware(StubApp(), mcp_client=client, resource_url=url), + ], +) +def test_a_resource_url_that_differs_from_the_audience_is_rejected(build, resource_url): + with pytest.raises(ConfigurationError, match="equal the client audience"): + build(McpServerClient(StubApiClient()), resource_url) + + +def test_an_explicit_resource_url_equal_to_the_audience_is_accepted(): + doc = build_protected_resource_metadata(McpServerClient(StubApiClient()), resource_url=RESOURCE_URL) + + assert doc["resource"] == RESOURCE_URL + + +@pytest.mark.parametrize( + "domain, expected", + [ + ("tenant.example", "https://tenant.example/"), + ("Tenant.Example", "https://tenant.example/"), + ("tenant.example:8443", "https://tenant.example:8443/"), + ], +) +def test_the_default_authorization_server_is_the_normalized_client_domain(domain, expected): + doc = build_protected_resource_metadata(McpServerClient(StubApiClient(domain=domain))) + + assert doc["authorization_servers"] == [expected] + + +def test_an_http_resource_with_a_port_is_accepted(): + doc = build_protected_resource_metadata(McpServerClient(StubApiClient(audience="http://localhost:3000/mcp"))) + + assert doc["resource"] == "http://localhost:3000/mcp" + + +@pytest.mark.parametrize("api_options", [{}, {"domain": None, "domains": ["a.example", "b.example"]}], ids=["single", "multi"]) +def test_explicit_authorization_servers_are_advertised_as_given(api_options): + servers = ["https://login.example/", "https://other.example/"] + + doc = build_protected_resource_metadata(McpServerClient(StubApiClient(**api_options)), authorization_servers=servers) + + assert doc["authorization_servers"] == servers + + +@pytest.mark.parametrize( + "servers", + [ + [], + "https://login.example/", + ["login.example"], + ["http://login.example/"], + ["https:///"], + ["https://login.example/#x"], + ["https://login.example/?q=1"], + ["https://user:pass@login.example/"], + ["https://login .example/"], + ["https://login.example/\x00"], + [None], + ], +) +def test_authorization_servers_must_be_a_non_empty_list_of_https_urls(servers): + with pytest.raises(ConfigurationError, match="authorization_servers"): + build_protected_resource_metadata(McpServerClient(StubApiClient()), authorization_servers=servers) + + +async def test_the_middleware_advertises_the_authorization_servers_it_is_given(): + middleware = _middleware(StubApp(), StubApiClient(), authorization_servers=["https://login.example/"]) + + sent = await _call(middleware, _scope(path="/.well-known/oauth-protected-resource", method="GET")) + + assert _body(sent)["authorization_servers"] == ["https://login.example/"] diff --git a/packages/auth0-ai-mcp/tests/test_principal.py b/packages/auth0-ai-mcp/tests/test_principal.py new file mode 100644 index 0000000..c85df56 --- /dev/null +++ b/packages/auth0-ai-mcp/tests/test_principal.py @@ -0,0 +1,142 @@ +import time + +import pytest +from auth0_api_python.errors import VerifyAccessTokenError + +from auth0_ai_mcp import Principal +from auth0_ai_mcp.principal import build_principal + + +def test_build_principal_with_all_claims_present(): + claims = { + "sub": "auth0|user123", + "exp": int(time.time()) + 3600, + "scope": "calendar:read calendar:write", + "permissions": ["calendar:read", "calendar:write"], + "client_id": "my_client_id", + "org_id": "org_abc123", + } + + principal = build_principal(claims) + + assert principal == Principal( + sub="auth0|user123", + expires_at=claims["exp"], + scopes=["calendar:read", "calendar:write"], + permissions=["calendar:read", "calendar:write"], + client_id="my_client_id", + org_id="org_abc123", + ) + + +def test_build_principal_permissions_present_but_empty(): + claims = { + "sub": "auth0|user123", + "exp": int(time.time()) + 3600, + "azp": "spa_client_id", + "permissions": [], + } + + principal = build_principal(claims) + + assert principal.permissions == [] + + +def test_build_principal_missing_both_org_id_and_permissions(): + claims = { + "sub": "auth0|user123", + "exp": int(time.time()) + 3600, + "azp": "spa_client_id", + } + + principal = build_principal(claims) + + assert principal.org_id is None + assert principal.permissions is None + + +def test_build_principal_missing_scope_claim_yields_empty_list(): + claims = { + "sub": "auth0|user123", + "exp": int(time.time()) + 3600, + "azp": "spa_client_id", + } + + principal = build_principal(claims) + + assert principal.scopes == [] + + +@pytest.mark.parametrize("claim", [{"permissions": ["read:x"]}, {"scp": ["read:x"]}]) +def test_build_principal_ignores_other_claims_when_scope_is_missing(claim): + claims = {"sub": "auth0|user123", "exp": int(time.time()) + 3600, "azp": "spa_client_id", **claim} + + assert build_principal(claims).scopes == [] + + +_ABSENT = object() + + +def _claims(**overrides): + claims = {"sub": "auth0|user123", "exp": int(time.time()) + 3600, "azp": "spa_client_id"} + claims.update(overrides) + return {k: v for k, v in claims.items() if v is not _ABSENT} + + +@pytest.mark.parametrize("sub", [_ABSENT, " ", "\t\n", 123, None]) +def test_build_principal_rejects_blank_or_non_string_sub(sub): + with pytest.raises(VerifyAccessTokenError, match="sub"): + build_principal(_claims(sub=sub)) + + +def test_build_principal_prefers_azp_over_client_id(): + principal = build_principal(_claims(azp="azp_value", client_id="client_id_value")) + + assert principal.client_id == "azp_value" + + +@pytest.mark.parametrize("azp", [_ABSENT, None, "", " ", 42]) +def test_build_principal_falls_back_to_client_id_when_azp_is_unusable(azp): + principal = build_principal(_claims(azp=azp, client_id="client_id_value")) + + assert principal.client_id == "client_id_value" + + +@pytest.mark.parametrize( + ("azp", "client_id"), + [(_ABSENT, _ABSENT), (None, None), (" ", " "), (42, 42), ("", ["c"])], +) +def test_build_principal_rejects_claims_without_a_usable_client(azp, client_id): + with pytest.raises(VerifyAccessTokenError, match="client_id"): + build_principal(_claims(azp=azp, client_id=client_id)) + + +@pytest.mark.parametrize("exp", [_ABSENT, None, "4102444800", True, False, float("nan"), float("inf"), [1]]) +def test_build_principal_rejects_missing_or_non_numeric_exp(exp): + with pytest.raises(VerifyAccessTokenError, match="exp"): + build_principal(_claims(exp=exp)) + + +def test_build_principal_accepts_a_float_exp(): + assert build_principal(_claims(exp=4102444800.5)).expires_at == 4102444800.5 + + +@pytest.mark.parametrize("permissions", ["read:x", {"read:x": True}, ["read:x", 1], [None]]) +def test_build_principal_rejects_permissions_that_are_not_a_list_of_strings(permissions): + with pytest.raises(VerifyAccessTokenError, match="permissions"): + build_principal(_claims(permissions=permissions)) + + +def test_build_principal_treats_null_permissions_as_absent(): + assert build_principal(_claims(permissions=None)).permissions is None + + +@pytest.mark.parametrize("org_id", [123, ["org_abc"], {"id": "org_abc"}, True]) +def test_build_principal_rejects_non_string_org_id(org_id): + with pytest.raises(VerifyAccessTokenError, match="org_id"): + build_principal(_claims(org_id=org_id)) + + +@pytest.mark.parametrize("scope", [["read:x"], 123, {"read:x": True}]) +def test_build_principal_non_string_scope_yields_empty_list(scope): + assert build_principal(_claims(scope=scope)).scopes == [] diff --git a/packages/auth0-ai-mcp/tests/test_request_context.py b/packages/auth0-ai-mcp/tests/test_request_context.py new file mode 100644 index 0000000..02227ed --- /dev/null +++ b/packages/auth0-ai-mcp/tests/test_request_context.py @@ -0,0 +1,89 @@ +"""Tests for the request context binding and the get_caller_* accessors.""" + +import asyncio + +import pytest + +from auth0_ai_mcp import ( + Principal, + RequestContext, + UnauthenticatedError, + bind_request_context, + get_caller_mcp_client, + get_caller_principal, + require_caller_mcp_client, + require_caller_principal, +) + + +def _context() -> RequestContext: + principal = Principal( + sub="u1", expires_at=9999999999, scopes=[], permissions=None, client_id="client_1", org_id=None + ) + # OBO is not exercised here, so a plain object stands in for the client. + return RequestContext(principal=principal, mcp_client=object()) # type: ignore[arg-type] + + +def test_context_is_set_inside_the_block(): + ctx = _context() + with bind_request_context(ctx): + assert get_caller_principal() is ctx.principal + assert get_caller_mcp_client() is ctx.mcp_client + + +def test_context_is_reset_after_normal_exit(): + with bind_request_context(_context()): + pass + assert get_caller_principal() is None + + +def test_nested_bind_restores_the_outer_context_on_exit(): + outer, inner = _context(), _context() + with bind_request_context(outer): + with bind_request_context(inner): + assert get_caller_principal() is inner.principal + assert get_caller_principal() is outer.principal + assert get_caller_principal() is None + + +async def test_concurrent_tasks_each_see_their_own_context(): + async def read_back(ctx): + with bind_request_context(ctx): + await asyncio.sleep(0) + return get_caller_principal() + + first, second = _context(), _context() + + seen = await asyncio.gather(read_back(first), read_back(second)) + + assert seen[0] is first.principal + assert seen[1] is second.principal + + +def test_context_is_reset_after_an_exception(): + with pytest.raises(RuntimeError), bind_request_context(_context()): + raise RuntimeError("boom") + assert get_caller_principal() is None + + +def test_require_caller_principal_returns_principal_inside_a_request(): + ctx = _context() + with bind_request_context(ctx): + assert require_caller_principal() is ctx.principal + + +def test_require_caller_principal_raises_unauthenticated_outside_a_request(): + with pytest.raises(UnauthenticatedError): + require_caller_principal() + + +def test_require_caller_mcp_client_returns_client_inside_a_request(): + ctx = _context() + with bind_request_context(ctx): + assert require_caller_mcp_client() is ctx.mcp_client + + +def test_require_caller_mcp_client_raises_unauthenticated_outside_a_request(): + assert get_caller_mcp_client() is None + with pytest.raises(UnauthenticatedError): + require_caller_mcp_client() diff --git a/packages/auth0-ai-mcp/tests/test_scopes.py b/packages/auth0-ai-mcp/tests/test_scopes.py new file mode 100644 index 0000000..049bfc2 --- /dev/null +++ b/packages/auth0-ai-mcp/tests/test_scopes.py @@ -0,0 +1,240 @@ +"""Tests for require_scopes and the with_required_scopes decorator.""" + +import importlib.util +import inspect +import sys + +import pytest +from auth0_api_python.errors import BaseAuthError, ConfigurationError +from mcp.server.mcpserver import MCPServer +from mcp.server.mcpserver.exceptions import ToolError, UnexpectedToolError + +from auth0_ai_mcp import Principal, errors +from auth0_ai_mcp.errors import InsufficientScopeError, UnauthenticatedError +from auth0_ai_mcp.request_context import ( + RequestContext, + bind_request_context, + reset_request_context, + set_request_context, +) +from auth0_ai_mcp.scopes import require_scopes, with_required_scopes + + +def _principal(scopes: list[str]) -> Principal: + return Principal( + sub="u1", + expires_at=9999999999, + scopes=scopes, + permissions=None, + client_id="client_1", + org_id=None, + ) + + +def _make_context(principal: Principal) -> RequestContext: + # mcp_client is required by the dataclass. Pass a plain object since OBO is not tested here. + return RequestContext(principal=principal, mcp_client=object()) # type: ignore[arg-type] + + +def test_passes_when_principal_has_all_scopes(): + require_scopes(_principal(["read:x", "write:y"]), ["read:x", "write:y"]) + + +def test_passes_with_superset_of_required_scopes(): + require_scopes(_principal(["read:x", "write:y", "admin"]), ["read:x"]) + + +def test_single_string_is_one_scope(): + require_scopes(_principal(["read:x"]), "read:x") + + +@pytest.mark.parametrize("required", ["write:y", ["read:x", "write:y"]]) +def test_raises_insufficient_scope_when_any_required_scope_is_missing(required): + with pytest.raises(InsufficientScopeError) as exc_info: + require_scopes(_principal(["read:x"]), required) + + assert exc_info.value.get_status_code() == 403 + assert exc_info.value.get_error_code() == "insufficient_scope" + + +@pytest.mark.parametrize( + ("granted", "required"), + [("read:x:all", "read:x"), ("READ:X", "read:x"), ("read:x", "read:x:all")], +) +def test_a_scope_that_only_looks_like_the_required_one_does_not_satisfy_the_check(granted, required): + with pytest.raises(InsufficientScopeError): + require_scopes(_principal([granted]), required) + + +def test_scope_held_only_as_a_permission_does_not_satisfy_the_check(): + principal = Principal( + sub="u1", expires_at=9999999999, scopes=[], permissions=["read:x"], client_id="client_1", org_id=None + ) + + with pytest.raises(InsufficientScopeError): + require_scopes(principal, "read:x") + + +def test_insufficient_scope_message_does_not_leak_scope(): + with pytest.raises(InsufficientScopeError) as exc_info: + require_scopes(_principal(["read:x"]), ["read:x", "write:y"]) + assert "write:y" not in str(exc_info.value) + + +def test_raises_unauthenticated_when_principal_is_none(): + with pytest.raises(UnauthenticatedError) as exc_info: + require_scopes(None, "read:x") + + assert exc_info.value.get_status_code() == 401 + assert exc_info.value.get_error_code() == "invalid_token" + + +@pytest.mark.parametrize("scopes", [[], "", " ", ["read:x", ""], ["read:x", " "], ["read:x", 1], None, ("read:x",)]) +def test_rejects_empty_or_malformed_scopes(scopes: object): + with pytest.raises(ConfigurationError): + require_scopes(_principal(["read:x"]), scopes) # type: ignore[arg-type] + + +@pytest.mark.parametrize("scopes", ["read:a write:a", "read:a ", ["read:a", "write:a read:b"]]) +def test_rejects_a_scope_that_contains_whitespace(scopes: object): + with pytest.raises(ConfigurationError): + require_scopes(_principal(["read:a"]), scopes) # type: ignore[arg-type] + with pytest.raises(ConfigurationError): + with_required_scopes(scopes) # type: ignore[arg-type] + + +@pytest.mark.parametrize(("required", "held"), [("read:x", ["read:x"]), (["read:x", "write:y"], ["read:x", "write:y"])]) +async def test_handler_runs_when_principal_has_every_required_scope(required, held): + ctx = _make_context(_principal(held)) + token = set_request_context(ctx) + try: + @with_required_scopes(required) + async def handler() -> str: + return "ok" + + result = await handler() + assert result == "ok" + finally: + reset_request_context(token) + + +async def test_args_and_kwargs_pass_through(): + ctx = _make_context(_principal(["read:x"])) + token = set_request_context(ctx) + try: + @with_required_scopes("read:x") + async def handler(a: int, *, b: str) -> tuple: + return (a, b) + + assert await handler(1, b="hello") == (1, "hello") + finally: + reset_request_context(token) + + +async def test_raises_insufficient_scope_and_inner_body_does_not_run(): + ctx = _make_context(_principal(["read:x"])) + token = set_request_context(ctx) + try: + inner_ran = False + + @with_required_scopes("write:y") + async def handler() -> None: + nonlocal inner_ran + inner_ran = True + + with pytest.raises(InsufficientScopeError): + await handler() + + # The guard must have fired before the body. inner_ran must still be False. + assert inner_ran is False + finally: + reset_request_context(token) + + +@pytest.mark.parametrize("scopes", [[], "", ["read:x", ""], ["read:x", 1]]) +def test_rejects_empty_or_malformed_scopes_at_decoration_time(scopes: object): + with pytest.raises(ConfigurationError): + with_required_scopes(scopes) # type: ignore[arg-type] + + +def test_rejects_a_sync_function_at_decoration_time(): + def sync_tool() -> str: + return "ran" + + with pytest.raises(ConfigurationError): + with_required_scopes("read:x")(sync_tool) # type: ignore[type-var] + + +async def test_raises_unauthenticated_when_no_context_is_set(): + # Do not set any context. The contextvar defaults to None. + @with_required_scopes("read:x") + async def handler() -> None: + pass # pragma: no cover + + with pytest.raises(UnauthenticatedError): + await handler() + + +def test_decorated_handler_keeps_its_name_doc_and_signature(): + @with_required_scopes("read:x") + async def handler(a: int, *, b: str = "x") -> str: + """Handler doc.""" + return b + + assert handler.__name__ == "handler" + assert handler.__doc__ == "Handler doc." + assert list(inspect.signature(handler).parameters) == ["a", "b"] + + +async def test_denied_tool_reaches_the_client_as_a_tool_error_with_the_reason(): + mcp = MCPServer("test") + + @mcp.tool() + @with_required_scopes("read:x") + async def guarded() -> str: + return "body" + + with bind_request_context(_make_context(_principal(["other"]))): + with pytest.raises(ToolError) as exc_info: + await mcp.call_tool("guarded", {}) + + assert not isinstance(exc_info.value, UnexpectedToolError) + assert "Missing one or more required scopes." in str(exc_info.value) + assert isinstance(exc_info.value.__cause__, InsufficientScopeError) + + +async def test_tool_without_a_caller_reaches_the_client_as_a_tool_error_with_the_reason(): + mcp = MCPServer("test") + + @mcp.tool() + @with_required_scopes("read:x") + async def guarded() -> str: + return "body" + + with pytest.raises(ToolError) as exc_info: + await mcp.call_tool("guarded", {}) + + assert not isinstance(exc_info.value, UnexpectedToolError) + assert "No principal in the current request context." in str(exc_info.value) + assert isinstance(exc_info.value.__cause__, UnauthenticatedError) + + +@pytest.mark.parametrize("error", [InsufficientScopeError, UnauthenticatedError]) +def test_in_tool_errors_are_both_auth_errors_and_tool_errors(error): + exc = error("denied") + + assert isinstance(exc, BaseAuthError) + assert isinstance(exc, ToolError) + assert str(exc) == "denied" + assert exc.get_status_code() in (401, 403) + + +def test_in_tool_errors_stay_auth_errors_when_mcp_is_not_installed(monkeypatch): + monkeypatch.setitem(sys.modules, "mcp.server.mcpserver.exceptions", None) + spec = importlib.util.spec_from_file_location("errors_without_mcp", errors.__file__) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + for error in (module.InsufficientScopeError, module.UnauthenticatedError): + assert issubclass(error, BaseAuthError) + assert not issubclass(error, ToolError)