From da13476d4f1ab756ae587287d6bc3891ef431169 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:09:49 -0400 Subject: [PATCH 01/18] adds fastmcp mcp example --- .gitignore | 1 - examples/example-fastmcp-mcp/.env.example | 5 + examples/example-fastmcp-mcp/README.md | 43 + examples/example-fastmcp-mcp/poetry.lock | 1184 +++++++++++++++++ examples/example-fastmcp-mcp/pyproject.toml | 19 + examples/example-fastmcp-mcp/src/__init__.py | 0 .../example-fastmcp-mcp/src/auth0/__init__.py | 55 + .../src/auth0/middeware.py | 95 ++ .../example-fastmcp-mcp/src/auth0/tools.py | 89 ++ examples/example-fastmcp-mcp/src/mcp.py | 10 + examples/example-fastmcp-mcp/src/server.py | 55 + examples/example-fastmcp-mcp/src/tools.py | 48 + 12 files changed, 1603 insertions(+), 1 deletion(-) create mode 100644 examples/example-fastmcp-mcp/.env.example create mode 100644 examples/example-fastmcp-mcp/README.md create mode 100644 examples/example-fastmcp-mcp/poetry.lock create mode 100644 examples/example-fastmcp-mcp/pyproject.toml create mode 100644 examples/example-fastmcp-mcp/src/__init__.py create mode 100644 examples/example-fastmcp-mcp/src/auth0/__init__.py create mode 100644 examples/example-fastmcp-mcp/src/auth0/middeware.py create mode 100644 examples/example-fastmcp-mcp/src/auth0/tools.py create mode 100644 examples/example-fastmcp-mcp/src/mcp.py create mode 100644 examples/example-fastmcp-mcp/src/server.py create mode 100644 examples/example-fastmcp-mcp/src/tools.py diff --git a/.gitignore b/.gitignore index c30eceb..707996e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ dist docs #testfile -server.py setup.py test.py test-script.py diff --git a/examples/example-fastmcp-mcp/.env.example b/examples/example-fastmcp-mcp/.env.example new file mode 100644 index 0000000..d776611 --- /dev/null +++ b/examples/example-fastmcp-mcp/.env.example @@ -0,0 +1,5 @@ +# Auth0 Configuration +AUTH0_DOMAIN=your-tenant.auth0.com +AUTH0_AUDIENCE=https://api.example.com +MCP_SERVER_URL=http://localhost:3001 +PORT=3001 \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/README.md b/examples/example-fastmcp-mcp/README.md new file mode 100644 index 0000000..faeb9c5 --- /dev/null +++ b/examples/example-fastmcp-mcp/README.md @@ -0,0 +1,43 @@ +# Example FastMCP MCP Server with Auth0 Integration + +This example demonstrates how to create a FastMCP MCP server that uses Auth0 for authentication using the `auth0-api-python` library. + +## Install dependencies + +``` +poetry install +``` + +## Auth0 Tenant Setup + +For detailed instructions on setting up your Auth0 tenant for MCP server integration, please refer to the [Auth0 Tenant Setup guide](https://github.com/auth0/auth0-auth-js/blob/main/examples/example-fastmcp-mcp/README.md#auth0-tenant-setup). + +## Configuration + +Rename `.env.example` to `.env` and configure the domain and audience: + +``` +# Auth0 tenant domain +AUTH0_DOMAIN=example-tenant.us.auth0.com + +# Auth0 API Identifier +AUTH0_AUDIENCE=http://localhost:3001 +``` + +With the configuration in place, the example can be started by running: + +```bash +poetry run python -m src.server +``` + +## Testing + +Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively: + +```bash +npx @modelcontextprotocol/inspector +``` + +The server will start up and the UI will be accessible at http://localhost:6274. + +In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL. diff --git a/examples/example-fastmcp-mcp/poetry.lock b/examples/example-fastmcp-mcp/poetry.lock new file mode 100644 index 0000000..96c0421 --- /dev/null +++ b/examples/example-fastmcp-mcp/poetry.lock @@ -0,0 +1,1184 @@ +# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. + +[[package]] +name = "ada-url" +version = "1.26.0" +description = "URL parser and manipulator based on the WHAT WG URL standard" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ada_url-1.26.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:c2d1578f592be814d40f0a56031809b40500f61cb240966d0ec25ba152b55eb0"}, + {file = "ada_url-1.26.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:321d581274a60f227609be9b6c0863eced4a31b5bf8219d72bf305710d58116d"}, + {file = "ada_url-1.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:071ca476ed5e35651cd39986faea45f100b338147d69218b8170d491d6345baf"}, + {file = "ada_url-1.26.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc2cb70aa714f4093d8406bef1c1ae8c998818dda4e512645e6fc802959fdf1"}, + {file = "ada_url-1.26.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f50ec59bd673941b4e9563e152d7917eda5859b834f2e63093dafecf9896d396"}, + {file = "ada_url-1.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9d27819cac073dcf0909f3d884198d107d7149ccd21f8c084aed5a6eb2d4e579"}, + {file = "ada_url-1.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ea2ffc0f8976d05217324844a0f18bb90c8ec6ac08c31646a4a4a6396e8af906"}, + {file = "ada_url-1.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:3eb5ce4b81d1f8344d032c69af1804bc1475ba4db3d5b586e6f1dae0884fcbcf"}, + {file = "ada_url-1.26.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:6eec591ed6c13b323501e2ce1f29f0dc731affb11036140119382baa08f17f3b"}, + {file = "ada_url-1.26.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:3f8298b60ddd76f2b225b4e5b16b5def61c157c1cdd856c2093b3fcaa3e98441"}, + {file = "ada_url-1.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba6d518b4bafec467c8d879a1620f0aef400307cb5ae0f96772139f66c611d56"}, + {file = "ada_url-1.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbd6fb73f182ec1488ce24716d64af5fbcc8af90a511a571ca408d8f91d36ad"}, + {file = "ada_url-1.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de7b083b700cb71490e9a716ea42c9fee3b4f973aeccde08c6e6066f1184f59a"}, + {file = "ada_url-1.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:272fa1ac5ec60bd1a5399c824e63bbd3084ab1410cb89c5497cc1b3e93513cf2"}, + {file = "ada_url-1.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c244dabc7d88861efee7e822525b89fdcc8fec7d17f89ca0368a90eb401c76d"}, + {file = "ada_url-1.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:db36fa791b80e2f1034c91a41ab489d3b78aead79f52173b67619bd830d3ff83"}, + {file = "ada_url-1.26.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5ad5fca18df30b93aa4196bc236aef37dfb4e8b1ade93deea14c03b9c2d87486"}, + {file = "ada_url-1.26.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:3a52d5e157738519ab504913972e3abf4e800a45574e9b431c4ee88589f213d5"}, + {file = "ada_url-1.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b56a0a685c7440aa7f49ff4827bca55c03ba4c54e7b9744a867d195eca2564b7"}, + {file = "ada_url-1.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f060bfdd2774c8313f0353325aeadf9afdd940f4c0833628d3fa4b7b09fe7949"}, + {file = "ada_url-1.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e23086b6f65d21a988457cad4cc63235796b1f213a66d7173d05206690fabb69"}, + {file = "ada_url-1.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f3d5b942b19a81236e1aae94bc7315fbeaceeefa2775c2f40ab9196009151da0"}, + {file = "ada_url-1.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4606887898806cc4cbf19a285a1dab131e2409dc293a534c8505ef15eeac7fb7"}, + {file = "ada_url-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:09b9a6e47d6084ac64957a947bfadab4fc1117b157cd0463091c46434bb11d01"}, + {file = "ada_url-1.26.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:023c8f520ba3a2a7c389f1205d4b2a9384bd06c8cf8b48ae58c43cdb4cfa1881"}, + {file = "ada_url-1.26.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:6e8db691157ada513c5e877fd66f0cea54ef473fcf7e6bad429608f2c32d5d63"}, + {file = "ada_url-1.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c158ae6850b1ac66c1dbd54a7b5dac5a2a953ca33db0cd6bcef1c97b1b5536de"}, + {file = "ada_url-1.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc723bf495730c22ec2890b8e5d4bbe591b73e97af6e8a862e0ca44ac4197660"}, + {file = "ada_url-1.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd375e12247a1d6ca190a67bc88463b60c013361d3f99e2347f8a7af2f548a1c"}, + {file = "ada_url-1.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:da783beac508b487d1c09a2afa35e3e14e39f164dd2c4a2d91db16ac63cfe65d"}, + {file = "ada_url-1.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:01e9e18dad01adc4703ffda5600c5ae0e5da547124e4ae0a74b0d30cdcf952b1"}, + {file = "ada_url-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:bbc9c955a37c15984495d487a9e1b5ff8aa681101cd3f087faab30fab03d53c8"}, + {file = "ada_url-1.26.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:e2a9fa9293f0137b04c1804fa357e906401977111cc8f7da2aa0e4971d152455"}, + {file = "ada_url-1.26.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:8678a5303a6d21d4c7639e8cb7d236f21a43615705ec841c2e0201a9c295de09"}, + {file = "ada_url-1.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf74c390d3ba9d264521d60e6c4a211aae9dbc5fd324c80bec8bfd5eea228347"}, + {file = "ada_url-1.26.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a7db1d9356dfaf3d69e8fe052ea2f301044b2ec111f050a16ea49ea53645f1"}, + {file = "ada_url-1.26.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2558db19ac40b1cd4d936b57724442a3340e4cd7b9ef55fa9b793fef525a12d7"}, + {file = "ada_url-1.26.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28542b0b958b75f5ce6cf3121f43eeb3884d29b6e873d5aad70c9a4807938178"}, + {file = "ada_url-1.26.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d60751f85392c2e1b610fb1799b6ec496e64b6c7072bac307c7109813da5745"}, + {file = "ada_url-1.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:3851518c53c8b5b2c2fb75a3571987f5669d2f37f7fe81e28e6080d42079f6e2"}, + {file = "ada_url-1.26.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:09ea872cc1d064123586ca3c0f934daf6d2bf0ed92dfdbebf166268ec1952595"}, + {file = "ada_url-1.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0b2df12d04f5a57d17175182fb631cc1c21b21d6a1174fc1dee26c9978cec39b"}, + {file = "ada_url-1.26.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc6d6ce54384cae19c599b4464c391ad5208f244c885cf150957aeec81102bb7"}, + {file = "ada_url-1.26.0-pp310-pypy310_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6f15abd4204760419683b7457ffbc4a71c86383b10273454db4773ad3e763c"}, + {file = "ada_url-1.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9c5018f739e2ddb092cd0f2a2a8ee0125bbf101ac93b1cbc762988b4515b1672"}, + {file = "ada_url-1.26.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:476b5ff71d89ce07ddc8d059c404f754582ad66946f6eb4ebb8a3162c917bc79"}, + {file = "ada_url-1.26.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d9e4fdc053711d42bb1ca3a1d1b201fa4628b6fdc8c65bdf158e9ec3ad1be0a"}, + {file = "ada_url-1.26.0-pp39-pypy39_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80bee51c57e53b878c1855b4c97c4037d5d1d35f83ade0f3664e82f2e9259ca3"}, + {file = "ada_url-1.26.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e666ea81c54d8c705fa6262ef502fa483d6ca48727c6340f488f98d1d4716147"}, + {file = "ada_url-1.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d4e1a6d4d60d3603233b4dd6c3e461d25768d7127c346fc6dcd83920a619500e"}, + {file = "ada_url-1.26.0.tar.gz", hash = "sha256:87988926d78a68bc08de0595362163fa3d3126bf9e0223aaf9d98272de2625f4"}, +] + +[package.dependencies] +cffi = "*" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + +[[package]] +name = "auth0-api-python" +version = "1.0.0.b5" +description = "SDK for verifying access tokens and securing APIs with Auth0, using Authlib." +optional = false +python-versions = "^3.9" +groups = ["main"] +files = [] +develop = true + +[package.dependencies] +ada-url = "^1.25.0" +authlib = "^1.0" +httpx = "^0.28.1" +requests = "^2.31.0" + +[package.source] +type = "directory" +url = "../.." + +[[package]] +name = "authlib" +version = "1.6.4" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796"}, + {file = "authlib-1.6.4.tar.gz", hash = "sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649"}, +] + +[package.dependencies] +cryptography = "*" + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +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.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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 = ["main"] +markers = "platform_system == \"Windows\"" +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 = "46.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.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b"}, + {file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab"}, + {file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75"}, + {file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5"}, + {file = "cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0"}, + {file = "cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7"}, + {file = "cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0"}, + {file = "cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b"}, + {file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657"}, + {file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0"}, + {file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0"}, + {file = "cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277"}, + {file = "cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05"}, + {file = "cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784"}, + {file = "cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca"}, + {file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc"}, + {file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7"}, + {file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a"}, + {file = "cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1"}, + {file = "cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3"}, + {file = "cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9"}, + {file = "cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d"}, + {file = "cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46"}, + {file = "cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a"}, + {file = "cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[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"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[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 = "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 = "httpx-sse" +version = "0.4.1" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, + {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, +] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "jsonschema" +version = "4.25.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, + {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[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"] +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"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "mcp" +version = "1.14.1" +description = "Model Context Protocol SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "mcp-1.14.1-py3-none-any.whl", hash = "sha256:3b7a479e8e5cbf5361bdc1da8bc6d500d795dc3aff44b44077a363a7f7e945a4"}, + {file = "mcp-1.14.1.tar.gz", hash = "sha256:31c4406182ba15e8f30a513042719c3f0a38c615e76188ee5a736aaa89e20134"}, +] + +[package.dependencies] +anyio = ">=4.5" +httpx = ">=0.27.1" +httpx-sse = ">=0.4" +jsonschema = ">=4.20.0" +pydantic = ">=2.11.0,<3.0.0" +pydantic-settings = ">=2.5.2" +python-multipart = ">=0.0.9" +pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""} +sse-starlette = ">=1.6.1" +starlette = ">=0.27" +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)"] +ws = ["websockets (>=15.0.1)"] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pydantic" +version = "2.11.9" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"}, + {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, + {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "referencing" +version = "0.36.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[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.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rpds-py" +version = "0.27.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, + {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1"}, + {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10"}, + {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808"}, + {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8"}, + {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9"}, + {file = "rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4"}, + {file = "rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1"}, + {file = "rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881"}, + {file = "rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a"}, + {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde"}, + {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21"}, + {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9"}, + {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948"}, + {file = "rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39"}, + {file = "rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15"}, + {file = "rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746"}, + {file = "rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90"}, + {file = "rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a"}, + {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444"}, + {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a"}, + {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1"}, + {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998"}, + {file = "rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39"}, + {file = "rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594"}, + {file = "rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502"}, + {file = "rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b"}, + {file = "rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d"}, + {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274"}, + {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd"}, + {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2"}, + {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002"}, + {file = "rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3"}, + {file = "rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83"}, + {file = "rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d"}, + {file = "rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228"}, + {file = "rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21"}, + {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef"}, + {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081"}, + {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd"}, + {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7"}, + {file = "rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688"}, + {file = "rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797"}, + {file = "rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334"}, + {file = "rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9"}, + {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60"}, + {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e"}, + {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212"}, + {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675"}, + {file = "rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3"}, + {file = "rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456"}, + {file = "rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3"}, + {file = "rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2"}, + {file = "rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48"}, + {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb"}, + {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734"}, + {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb"}, + {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0"}, + {file = "rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a"}, + {file = "rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772"}, + {file = "rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527"}, + {file = "rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e"}, + {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e"}, + {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786"}, + {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec"}, + {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b"}, + {file = "rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52"}, + {file = "rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b"}, + {file = "rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6"}, + {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c"}, + {file = "rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859"}, + {file = "rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +description = "SSE plugin for Starlette" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"}, + {file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"}, +] + +[package.dependencies] +anyio = ">=4.7.0" + +[package.extras] +daphne = ["daphne (>=4.2.0)"] +examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"] +granian = ["granian (>=2.3.1)"] +uvicorn = ["uvicorn (>=0.34.0)"] + +[[package]] +name = "starlette" +version = "0.48.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659"}, + {file = "starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.36.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uvicorn-0.36.0-py3-none-any.whl", hash = "sha256:6bb4ba67f16024883af8adf13aba3a9919e415358604ce46780d3f9bdc36d731"}, + {file = "uvicorn-0.36.0.tar.gz", hash = "sha256:527dc68d77819919d90a6b267be55f0e76704dca829d34aea9480be831a9b9d9"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "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.13)", "websockets (>=10.4)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "2f284fdb69e1df4b7908feefe7c91f608a9cf226e606800044ad213a718bf178" diff --git a/examples/example-fastmcp-mcp/pyproject.toml b/examples/example-fastmcp-mcp/pyproject.toml new file mode 100644 index 0000000..f68408c --- /dev/null +++ b/examples/example-fastmcp-mcp/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "example-fastmcp-mcp" +version = "0.1.0" +description = "" +authors = ["Auth0 "] +readme = "README.md" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.10" +python-dotenv = "^1.1.1" +mcp = "^1.14.1" +auth0-api-python = {path = "../..", develop = true} +starlette = "^0.48.0" +uvicorn = "^0.36.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/__init__.py b/examples/example-fastmcp-mcp/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py new file mode 100644 index 0000000..dc8c19e --- /dev/null +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -0,0 +1,55 @@ +""" +Auth0 integration for MCP server. + +This module provides Auth0 authentication and authorization for MCP servers, +including token verification, middleware, and scoped tool decorators. +""" + +import os +from typing import List +from pydantic import AnyHttpUrl +from dotenv import load_dotenv +from mcp.server.auth.routes import create_protected_resource_routes +from starlette.routing import Router, Route +from starlette.middleware import Middleware +from .middeware import Auth0Middleware + +# Load environment variables +load_dotenv() + +class Auth0Mcp: + def __init__(self, name: str): + self.name = name + self.audience = os.getenv("AUTH0_AUDIENCE", "https://api.example.com") + self.domain = os.getenv("AUTH0_DOMAIN", "your-tenant.auth0.com") + + def auth_metadata_router(self) -> Router: + """ + Returns a router that serves the OAuth Protected Resource Metadata + at the standard endpoint: /.well-known/oauth-protected-resource + """ + routes: List[Route] = [] + + routes = create_protected_resource_routes( + resource_url=AnyHttpUrl(self.audience), + authorization_servers=[AnyHttpUrl(f"https://{self.domain}")], + scopes_supported=[ + "openid", + "profile", + "email", + ], + resource_name=self.name, + ) + + return Router(routes=routes) + + def auth_middleware(self) -> list[Middleware]: + middleware: list[Middleware] = [] + + middleware.append( + Middleware( + Auth0Middleware + ) + ) + + return middleware \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/auth0/middeware.py b/examples/example-fastmcp-mcp/src/auth0/middeware.py new file mode 100644 index 0000000..d5057ef --- /dev/null +++ b/examples/example-fastmcp-mcp/src/auth0/middeware.py @@ -0,0 +1,95 @@ +import logging +import os +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.types import ASGIApp + +from auth0_api_python import ApiClient, ApiClientOptions +from auth0_api_python.errors import VerifyAccessTokenError + +logger = logging.getLogger(__name__) + +class Auth0Middleware(BaseHTTPMiddleware): + """ + Middleware that requires a valid Bearer token in the Authorization header. + This will validate the token using Auth0 SDK Client and add the auth info to request.scope["auth"]. + """ + + def __init__(self, app: ASGIApp): + super().__init__(app) + self.client = ApiClient(ApiClientOptions( + domain=os.getenv("AUTH0_DOMAIN", "your-tenant.auth0.com"), + audience=os.getenv("AUTH0_AUDIENCE", "https://api.example.com") + )) + + async def dispatch(self, request: Request, call_next): + # Extract Authorization header + auth_header = request.headers.get("authorization") + if not auth_header: + return self._return_auth_error_response(status_code=401, error="Authentication required", description="Missing Authorization header") + if not auth_header.lower().startswith("bearer "): + return self._return_auth_error_response( + status_code=401, + error="Authentication required", + description="Invalid Authorization header format" + ) + + # Extract and verify token + token = auth_header[7:] # Remove "Bearer " prefix + try: + decoded_and_verified_token = await self.client.verify_access_token( + token, + required_claims=["sub"] + ) + + # Check for client_id or azp + clientId = decoded_and_verified_token.get('client_id') or decoded_and_verified_token.get('azp') + if not clientId: + raise VerifyAccessTokenError("Token is missing 'client_id' or 'azp' claim") + + # Set up authentication context + auth_data = { + "token": token, + "client_id": clientId, + "scopes": decoded_and_verified_token.get("scope", "").split() + if decoded_and_verified_token.get("scope") else [] + } + + if decoded_and_verified_token.get('exp'): + auth_data["expiresAt"] = decoded_and_verified_token.get('exp') + + extra = {"sub": decoded_and_verified_token.get('sub'), "client_id": clientId} + + for field in ['azp', 'name', 'email']: + if decoded_and_verified_token.get(field): + extra[field] = decoded_and_verified_token.get(field) + + auth_data["extra"] = extra + request.scope["auth"] = auth_data + + return await call_next(request) + except VerifyAccessTokenError as e: + logger.error(f"Token verification failed: {str(e)}") + return self._return_auth_error_response( + status_code=401, + error="Authentication failed", + description="Invalid token" + ) + except Exception as e: + logger.error(f"Unexpected error in middleware: {str(e)}") + return self._return_auth_error_response( + status_code=500, + error="Internal Server Error", + description="Internal Server Error" + ) + + def _return_auth_error_response(self, status_code: int, error: str, description: str) -> JSONResponse: + www_auth_parts = [f'error="{error}"', f'error_description="{description}"', f'resource_metadata="{os.getenv("MCP_SERVER_URL")}"'] + www_authenticate = f"Bearer {', '.join(www_auth_parts)}" + + return JSONResponse( + status_code=status_code, + content={"error": error, "error_description": description}, + headers={"WWW-Authenticate": www_authenticate} + ) \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/auth0/tools.py b/examples/example-fastmcp-mcp/src/auth0/tools.py new file mode 100644 index 0000000..0dfb8d7 --- /dev/null +++ b/examples/example-fastmcp-mcp/src/auth0/tools.py @@ -0,0 +1,89 @@ +""" +Scope-based auth decorators for MCP tools. + +Provides a decorator with Auth0 scope checking for MCP tools. +""" +from functools import wraps +from typing import List, Callable +import asyncio + +def create_scoped_tool_decorator(mcp_server): + """Factory function to create a scoped_tool decorator bound to a MCP server instance.""" + + def scoped_tool( + required_scopes: List[str], + **tool_kwargs + ): + """ + Decorator that combines FastMCP tool registration with Auth0 scope checking. + + Args: + required_scopes: List of scopes required to use this tool + **tool_kwargs: Additional parameters passed to @mcp.tool() + + Example: + @scoped_tool(required_scopes=["read:data", "write:data"]) + def sensitive_tool(data: str, ctx: Context) -> str: + return f"Processing: {data}" + """ + def decorator(func: Callable) -> Callable: + @wraps(func) + async def scope_checked_wrapper(*args, **kwargs): + # Find the Context parameter in kwargs + ctx = None + for key, value in kwargs.items(): + if hasattr(value, 'request_context') and hasattr(value, 'fastmcp'): + ctx = value + break + + if not ctx: + raise Exception(f"Tool '{func.__name__}' requires a Context parameter for scope checking") + + # Get auth info and check scopes + try: + request = ctx.request_context.request + auth_info = get_auth_info(request) + + if not auth_info or auth_info == {}: + raise Exception("Authentication required to use this tool") + + user_scopes = auth_info.get("scopes", []) + client_id = auth_info.get("client_id", "unknown") + + # Check if user has all required scopes + missing_scopes = [scope for scope in required_scopes if scope not in user_scopes] + if missing_scopes: + await ctx.error(f"Access denied: missing required scopes {missing_scopes}") + raise Exception( + f"Missing required scopes for tool '{func.__name__}': {missing_scopes}." + ) + + # Log successful scope check + await ctx.info(f"Tool '{func.__name__}' authorized for client '{client_id}' with scopes: {user_scopes}") + + except Exception as e: + # Log other unexpected errors and wrap them + await ctx.error(f"Authorization check failed for tool '{func.__name__}': {str(e)}") + raise Exception(f"Authorization check failed: {str(e)}") + + # Call the original function + if asyncio.iscoroutinefunction(func): + return await func(*args, **kwargs) + else: + return func(*args, **kwargs) + + # Register the wrapped function as an MCP tool + mcp_server.add_tool( + scope_checked_wrapper, + **tool_kwargs + ) + return scope_checked_wrapper + + return decorator + + return scoped_tool + + +def get_auth_info(request) -> dict: + """Get authentication info from request.""" + return request.scope.get("auth", {}) \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/mcp.py b/examples/example-fastmcp-mcp/src/mcp.py new file mode 100644 index 0000000..c0d4a16 --- /dev/null +++ b/examples/example-fastmcp-mcp/src/mcp.py @@ -0,0 +1,10 @@ +""" +FastMCP server instance for Auth0 protected MCP server. +""" + +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP( + name="Auth0 Protected MCP Server", + stateless_http=True, +) \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py new file mode 100644 index 0000000..3a1d690 --- /dev/null +++ b/examples/example-fastmcp-mcp/src/server.py @@ -0,0 +1,55 @@ +import os +import logging +import contextlib +from collections.abc import AsyncIterator + +from starlette.applications import Starlette +from starlette.routing import Mount +from starlette.middleware.cors import CORSMiddleware + +from .auth0 import Auth0Mcp +from .mcp import mcp + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +auth0_mcp = Auth0Mcp(name="Example FastMCP Server") + +@contextlib.asynccontextmanager +async def lifespan(app: Starlette) -> AsyncIterator[None]: + async with contextlib.AsyncExitStack() as stack: + await stack.enter_async_context(mcp.session_manager.run()) + + # Import tools here to ensure tools are loaded after mcp is initialized + from . import tools + yield + +starlette_app = Starlette( + debug=True, + routes=[ + # Add discovery metadata route + *auth0_mcp.auth_metadata_router().routes, + + # Main MCP app route with authentication middleware + Mount( + "/", + app=mcp.streamable_http_app(), + middleware=auth0_mcp.auth_middleware() + ), + ], + lifespan=lifespan, +) + +# Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header +# for browser-based clients (ensures 500 errors get proper CORS headers) +app = CORSMiddleware( + starlette_app, + allow_origins=["*"], # Adjust as needed for production + allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods + expose_headers=["Mcp-Session-Id"], +) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, port=int(os.getenv("PORT", "3001"))) diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py new file mode 100644 index 0000000..f6874da --- /dev/null +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -0,0 +1,48 @@ +from json import dumps as jsonDumps +from .mcp import mcp +from .auth0.tools import get_auth_info +from mcp.server.fastmcp import Context +from .auth0.tools import create_scoped_tool_decorator + +# Create a scoped_tool decorator bound to the mcp instance +scoped_tool = create_scoped_tool_decorator(mcp) + +# Tool without required scopes +@mcp.tool() +def echo(text: str) -> str: + """Echoes the input text""" + return text + +# A MCP tool with required scopes +@scoped_tool( + required_scopes=["tool:greet"], + name="greet", + title="Greet Tool", + description="Greets a user", + annotations={"readOnlyHint": True} +) +def greet(name: str, ctx: Context) -> str: + if not name or name.strip() == "": + name = "world" + request = ctx.request_context.request + auth_info = get_auth_info(request) + user_id = auth_info.get("extra", {}).get("sub") + return f"Hello, {name}! You are authenticated as {user_id}" + +# A MCP tool with required scopes +@scoped_tool( + required_scopes=["tool:whoami"], + name="whoami", + title="Who Am I Tool", + description="Returns information about the authenticated user", + annotations={"readOnlyHint": True} +) +def whoami(ctx: Context) -> str: + request = ctx.request_context.request + auth_info = get_auth_info(request) + + response_data = { + "user": auth_info.get("extra", {}), + "scopes": auth_info.get("scopes", []), + } + return jsonDumps(response_data, indent=2) From a2f471c02b811b4cf88038206b3b4aa6415341dc Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:26:08 -0400 Subject: [PATCH 02/18] fix linting --- examples/example-fastmcp-mcp/poetry.lock | 29 +++++++++++++++++++ examples/example-fastmcp-mcp/pyproject.toml | 6 +++- .../example-fastmcp-mcp/src/auth0/__init__.py | 13 +++++---- .../src/auth0/middeware.py | 5 ++-- .../example-fastmcp-mcp/src/auth0/tools.py | 23 ++++++++------- examples/example-fastmcp-mcp/src/mcp.py | 2 +- examples/example-fastmcp-mcp/src/server.py | 8 ++--- examples/example-fastmcp-mcp/src/tools.py | 7 +++-- 8 files changed, 66 insertions(+), 27 deletions(-) diff --git a/examples/example-fastmcp-mcp/poetry.lock b/examples/example-fastmcp-mcp/poetry.lock index 96c0421..ee62054 100644 --- a/examples/example-fastmcp-mcp/poetry.lock +++ b/examples/example-fastmcp-mcp/poetry.lock @@ -1061,6 +1061,35 @@ files = [ {file = "rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8"}, ] +[[package]] +name = "ruff" +version = "0.13.1" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b"}, + {file = "ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334"}, + {file = "ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a"}, + {file = "ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783"}, + {file = "ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a"}, + {file = "ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700"}, + {file = "ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae"}, + {file = "ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317"}, + {file = "ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0"}, + {file = "ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5"}, + {file = "ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a"}, + {file = "ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51"}, +] + [[package]] name = "sniffio" version = "1.3.1" diff --git a/examples/example-fastmcp-mcp/pyproject.toml b/examples/example-fastmcp-mcp/pyproject.toml index f68408c..0ebc56b 100644 --- a/examples/example-fastmcp-mcp/pyproject.toml +++ b/examples/example-fastmcp-mcp/pyproject.toml @@ -16,4 +16,8 @@ uvicorn = "^0.36.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +build-backend = "poetry.core.masonry.api" +[dependency-groups] +dev = [ + "ruff (>=0.13.1,<0.14.0)" +] diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index dc8c19e..3d65816 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -6,12 +6,13 @@ """ import os -from typing import List -from pydantic import AnyHttpUrl + from dotenv import load_dotenv from mcp.server.auth.routes import create_protected_resource_routes -from starlette.routing import Router, Route +from pydantic import AnyHttpUrl from starlette.middleware import Middleware +from starlette.routing import Route, Router + from .middeware import Auth0Middleware # Load environment variables @@ -28,7 +29,7 @@ def auth_metadata_router(self) -> Router: Returns a router that serves the OAuth Protected Resource Metadata at the standard endpoint: /.well-known/oauth-protected-resource """ - routes: List[Route] = [] + routes: list[Route] = [] routes = create_protected_resource_routes( resource_url=AnyHttpUrl(self.audience), @@ -42,7 +43,7 @@ def auth_metadata_router(self) -> Router: ) return Router(routes=routes) - + def auth_middleware(self) -> list[Middleware]: middleware: list[Middleware] = [] @@ -52,4 +53,4 @@ def auth_middleware(self) -> list[Middleware]: ) ) - return middleware \ No newline at end of file + return middleware diff --git a/examples/example-fastmcp-mcp/src/auth0/middeware.py b/examples/example-fastmcp-mcp/src/auth0/middeware.py index d5057ef..ffa11c3 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middeware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middeware.py @@ -1,5 +1,6 @@ import logging import os + from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import JSONResponse @@ -52,7 +53,7 @@ async def dispatch(self, request: Request, call_next): auth_data = { "token": token, "client_id": clientId, - "scopes": decoded_and_verified_token.get("scope", "").split() + "scopes": decoded_and_verified_token.get("scope", "").split() if decoded_and_verified_token.get("scope") else [] } @@ -92,4 +93,4 @@ def _return_auth_error_response(self, status_code: int, error: str, description: status_code=status_code, content={"error": error, "error_description": description}, headers={"WWW-Authenticate": www_authenticate} - ) \ No newline at end of file + ) diff --git a/examples/example-fastmcp-mcp/src/auth0/tools.py b/examples/example-fastmcp-mcp/src/auth0/tools.py index 0dfb8d7..f171256 100644 --- a/examples/example-fastmcp-mcp/src/auth0/tools.py +++ b/examples/example-fastmcp-mcp/src/auth0/tools.py @@ -3,24 +3,27 @@ Provides a decorator with Auth0 scope checking for MCP tools. """ -from functools import wraps -from typing import List, Callable import asyncio +from functools import wraps +from typing import Callable + +from mcp.server.fastmcp import Context + def create_scoped_tool_decorator(mcp_server): """Factory function to create a scoped_tool decorator bound to a MCP server instance.""" - + def scoped_tool( - required_scopes: List[str], + required_scopes: list[str], **tool_kwargs ): """ Decorator that combines FastMCP tool registration with Auth0 scope checking. - + Args: required_scopes: List of scopes required to use this tool **tool_kwargs: Additional parameters passed to @mcp.tool() - + Example: @scoped_tool(required_scopes=["read:data", "write:data"]) def sensitive_tool(data: str, ctx: Context) -> str: @@ -31,8 +34,8 @@ def decorator(func: Callable) -> Callable: async def scope_checked_wrapper(*args, **kwargs): # Find the Context parameter in kwargs ctx = None - for key, value in kwargs.items(): - if hasattr(value, 'request_context') and hasattr(value, 'fastmcp'): + for value in kwargs.values(): + if isinstance(value, Context): ctx = value break @@ -60,7 +63,7 @@ async def scope_checked_wrapper(*args, **kwargs): # Log successful scope check await ctx.info(f"Tool '{func.__name__}' authorized for client '{client_id}' with scopes: {user_scopes}") - + except Exception as e: # Log other unexpected errors and wrap them await ctx.error(f"Authorization check failed for tool '{func.__name__}': {str(e)}") @@ -86,4 +89,4 @@ async def scope_checked_wrapper(*args, **kwargs): def get_auth_info(request) -> dict: """Get authentication info from request.""" - return request.scope.get("auth", {}) \ No newline at end of file + return request.scope.get("auth", {}) diff --git a/examples/example-fastmcp-mcp/src/mcp.py b/examples/example-fastmcp-mcp/src/mcp.py index c0d4a16..12f14ac 100644 --- a/examples/example-fastmcp-mcp/src/mcp.py +++ b/examples/example-fastmcp-mcp/src/mcp.py @@ -7,4 +7,4 @@ mcp = FastMCP( name="Auth0 Protected MCP Server", stateless_http=True, -) \ No newline at end of file +) diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py index 3a1d690..9603ed8 100644 --- a/examples/example-fastmcp-mcp/src/server.py +++ b/examples/example-fastmcp-mcp/src/server.py @@ -1,11 +1,11 @@ -import os -import logging import contextlib +import logging +import os from collections.abc import AsyncIterator from starlette.applications import Starlette -from starlette.routing import Mount from starlette.middleware.cors import CORSMiddleware +from starlette.routing import Mount from .auth0 import Auth0Mcp from .mcp import mcp @@ -22,7 +22,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: await stack.enter_async_context(mcp.session_manager.run()) # Import tools here to ensure tools are loaded after mcp is initialized - from . import tools + from . import tools # noqa: F401, I001 yield starlette_app = Starlette( diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index f6874da..457518a 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -1,8 +1,9 @@ from json import dumps as jsonDumps -from .mcp import mcp -from .auth0.tools import get_auth_info + from mcp.server.fastmcp import Context -from .auth0.tools import create_scoped_tool_decorator + +from .auth0.tools import create_scoped_tool_decorator, get_auth_info +from .mcp import mcp # Create a scoped_tool decorator bound to the mcp instance scoped_tool = create_scoped_tool_decorator(mcp) From 09fdc6d6142630e1c97405b94e7b38423ed3e3c1 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:47:28 -0400 Subject: [PATCH 03/18] fix lint --- examples/example-fastmcp-mcp/src/auth0/middeware.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/middeware.py b/examples/example-fastmcp-mcp/src/auth0/middeware.py index ffa11c3..cb80856 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middeware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middeware.py @@ -1,14 +1,13 @@ import logging import os +from auth0_api_python import ApiClient, ApiClientOptions +from auth0_api_python.errors import VerifyAccessTokenError from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import JSONResponse from starlette.types import ASGIApp -from auth0_api_python import ApiClient, ApiClientOptions -from auth0_api_python.errors import VerifyAccessTokenError - logger = logging.getLogger(__name__) class Auth0Middleware(BaseHTTPMiddleware): From 674dbd02657aee50adbfa0ca1ec82872a800885c Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:20:07 -0400 Subject: [PATCH 04/18] more cleaning up --- examples/example-fastmcp-mcp/.env.example | 3 +- .../example-fastmcp-mcp/src/auth0/__init__.py | 34 ++++++------------- .../src/auth0/{middeware.py => middleware.py} | 13 +++---- .../example-fastmcp-mcp/src/auth0/tools.py | 4 +-- examples/example-fastmcp-mcp/src/server.py | 11 ++++-- examples/example-fastmcp-mcp/src/tools.py | 3 +- 6 files changed, 31 insertions(+), 37 deletions(-) rename examples/example-fastmcp-mcp/src/auth0/{middeware.py => middleware.py} (90%) diff --git a/examples/example-fastmcp-mcp/.env.example b/examples/example-fastmcp-mcp/.env.example index d776611..de14826 100644 --- a/examples/example-fastmcp-mcp/.env.example +++ b/examples/example-fastmcp-mcp/.env.example @@ -2,4 +2,5 @@ AUTH0_DOMAIN=your-tenant.auth0.com AUTH0_AUDIENCE=https://api.example.com MCP_SERVER_URL=http://localhost:3001 -PORT=3001 \ No newline at end of file +PORT=3001 +DEBUG=false \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 3d65816..ca0bdde 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -5,35 +5,29 @@ including token verification, middleware, and scoped tool decorators. """ -import os - -from dotenv import load_dotenv from mcp.server.auth.routes import create_protected_resource_routes -from pydantic import AnyHttpUrl from starlette.middleware import Middleware from starlette.routing import Route, Router -from .middeware import Auth0Middleware +from .middleware import Auth0Middleware -# Load environment variables -load_dotenv() class Auth0Mcp: - def __init__(self, name: str): + def __init__(self, name: str, audience: str, domain: str): self.name = name - self.audience = os.getenv("AUTH0_AUDIENCE", "https://api.example.com") - self.domain = os.getenv("AUTH0_DOMAIN", "your-tenant.auth0.com") + self.audience = audience + self.domain = domain + if not self.audience or not self.domain: + raise RuntimeError("audience and domain must be provided") def auth_metadata_router(self) -> Router: """ Returns a router that serves the OAuth Protected Resource Metadata at the standard endpoint: /.well-known/oauth-protected-resource """ - routes: list[Route] = [] - - routes = create_protected_resource_routes( - resource_url=AnyHttpUrl(self.audience), - authorization_servers=[AnyHttpUrl(f"https://{self.domain}")], + routes: list[Route] = create_protected_resource_routes( + resource_url=self.audience, + authorization_servers=[f"https://{self.domain}"], scopes_supported=[ "openid", "profile", @@ -45,12 +39,4 @@ def auth_metadata_router(self) -> Router: return Router(routes=routes) def auth_middleware(self) -> list[Middleware]: - middleware: list[Middleware] = [] - - middleware.append( - Middleware( - Auth0Middleware - ) - ) - - return middleware + return [Middleware(Auth0Middleware, domain=self.domain, audience=self.audience)] diff --git a/examples/example-fastmcp-mcp/src/auth0/middeware.py b/examples/example-fastmcp-mcp/src/auth0/middleware.py similarity index 90% rename from examples/example-fastmcp-mcp/src/auth0/middeware.py rename to examples/example-fastmcp-mcp/src/auth0/middleware.py index cb80856..39f89ab 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middeware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middleware.py @@ -13,14 +13,16 @@ class Auth0Middleware(BaseHTTPMiddleware): """ Middleware that requires a valid Bearer token in the Authorization header. - This will validate the token using Auth0 SDK Client and add the auth info to request.scope["auth"]. + Validates the token using Auth0 SDK Client and stores auth info in request.state.auth. """ - def __init__(self, app: ASGIApp): + def __init__(self, app: ASGIApp, domain: str, audience: str): super().__init__(app) + if not domain or not audience: + raise RuntimeError("domain and audience must be provided") self.client = ApiClient(ApiClientOptions( - domain=os.getenv("AUTH0_DOMAIN", "your-tenant.auth0.com"), - audience=os.getenv("AUTH0_AUDIENCE", "https://api.example.com") + domain=domain, + audience=audience )) async def dispatch(self, request: Request, call_next): @@ -50,7 +52,6 @@ async def dispatch(self, request: Request, call_next): # Set up authentication context auth_data = { - "token": token, "client_id": clientId, "scopes": decoded_and_verified_token.get("scope", "").split() if decoded_and_verified_token.get("scope") else [] @@ -66,7 +67,7 @@ async def dispatch(self, request: Request, call_next): extra[field] = decoded_and_verified_token.get(field) auth_data["extra"] = extra - request.scope["auth"] = auth_data + request.state.auth = auth_data return await call_next(request) except VerifyAccessTokenError as e: diff --git a/examples/example-fastmcp-mcp/src/auth0/tools.py b/examples/example-fastmcp-mcp/src/auth0/tools.py index f171256..78222d9 100644 --- a/examples/example-fastmcp-mcp/src/auth0/tools.py +++ b/examples/example-fastmcp-mcp/src/auth0/tools.py @@ -88,5 +88,5 @@ async def scope_checked_wrapper(*args, **kwargs): def get_auth_info(request) -> dict: - """Get authentication info from request.""" - return request.scope.get("auth", {}) + """Get authentication info from request state.""" + return getattr(request.state, 'auth', {}) diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py index 9603ed8..520f73a 100644 --- a/examples/example-fastmcp-mcp/src/server.py +++ b/examples/example-fastmcp-mcp/src/server.py @@ -3,6 +3,7 @@ import os from collections.abc import AsyncIterator +from dotenv import load_dotenv from starlette.applications import Starlette from starlette.middleware.cors import CORSMiddleware from starlette.routing import Mount @@ -10,11 +11,17 @@ from .auth0 import Auth0Mcp from .mcp import mcp +load_dotenv() + # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -auth0_mcp = Auth0Mcp(name="Example FastMCP Server") +auth0_mcp = Auth0Mcp( + name="Example FastMCP Server", + audience=os.getenv("AUTH0_AUDIENCE"), + domain=os.getenv("AUTH0_DOMAIN") +) @contextlib.asynccontextmanager async def lifespan(app: Starlette) -> AsyncIterator[None]: @@ -26,7 +33,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: yield starlette_app = Starlette( - debug=True, + debug=os.getenv("DEBUG", "true").lower() == "true", routes=[ # Add discovery metadata route *auth0_mcp.auth_metadata_router().routes, diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index 457518a..3d536e7 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -23,8 +23,7 @@ def echo(text: str) -> str: annotations={"readOnlyHint": True} ) def greet(name: str, ctx: Context) -> str: - if not name or name.strip() == "": - name = "world" + name = (name or "").strip() or "world" request = ctx.request_context.request auth_info = get_auth_info(request) user_id = auth_info.get("extra", {}).get("sub") From 15de7ee2ae9690acaea9098a9a7e307d93075993 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:44:12 -0400 Subject: [PATCH 05/18] more clean up --- .../example-fastmcp-mcp/src/auth0/__init__.py | 26 ++++-- .../example-fastmcp-mcp/src/auth0/tools.py | 9 ++- examples/example-fastmcp-mcp/src/mcp.py | 10 --- examples/example-fastmcp-mcp/src/server.py | 10 +-- examples/example-fastmcp-mcp/src/tools.py | 80 ++++++++++--------- 5 files changed, 74 insertions(+), 61 deletions(-) delete mode 100644 examples/example-fastmcp-mcp/src/mcp.py diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index ca0bdde..1bec722 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -6,6 +6,7 @@ """ from mcp.server.auth.routes import create_protected_resource_routes +from mcp.server.fastmcp import FastMCP from starlette.middleware import Middleware from starlette.routing import Route, Router @@ -19,6 +20,15 @@ def __init__(self, name: str, audience: str, domain: str): self.domain = domain if not self.audience or not self.domain: raise RuntimeError("audience and domain must be provided") + self.mcp = FastMCP( + name="Auth0 Protected MCP Server", + stateless_http=True, + ) + self._scopes_supported = { + "openid", + "profile", + "email" + } def auth_metadata_router(self) -> Router: """ @@ -28,11 +38,7 @@ def auth_metadata_router(self) -> Router: routes: list[Route] = create_protected_resource_routes( resource_url=self.audience, authorization_servers=[f"https://{self.domain}"], - scopes_supported=[ - "openid", - "profile", - "email", - ], + scopes_supported=list(self._scopes_supported), resource_name=self.name, ) @@ -40,3 +46,13 @@ def auth_metadata_router(self) -> Router: def auth_middleware(self) -> list[Middleware]: return [Middleware(Auth0Middleware, domain=self.domain, audience=self.audience)] + + def register_scopes(self, scopes: list[str]) -> None: + """ + Register scopes that tools require. + + Args: + scopes: List of scopes to register (e.g., ["tool:greet", "tool:whoami"]) + """ + if scopes: + self._scopes_supported.update(scopes) \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/auth0/tools.py b/examples/example-fastmcp-mcp/src/auth0/tools.py index 78222d9..45b096a 100644 --- a/examples/example-fastmcp-mcp/src/auth0/tools.py +++ b/examples/example-fastmcp-mcp/src/auth0/tools.py @@ -10,7 +10,7 @@ from mcp.server.fastmcp import Context -def create_scoped_tool_decorator(mcp_server): +def create_scoped_tool_decorator(auth0Mcp): """Factory function to create a scoped_tool decorator bound to a MCP server instance.""" def scoped_tool( @@ -29,6 +29,11 @@ def scoped_tool( def sensitive_tool(data: str, ctx: Context) -> str: return f"Processing: {data}" """ + + if required_scopes: + # register scopes for PRM + auth0Mcp.register_scopes(required_scopes) + def decorator(func: Callable) -> Callable: @wraps(func) async def scope_checked_wrapper(*args, **kwargs): @@ -76,7 +81,7 @@ async def scope_checked_wrapper(*args, **kwargs): return func(*args, **kwargs) # Register the wrapped function as an MCP tool - mcp_server.add_tool( + auth0Mcp.mcp.add_tool( scope_checked_wrapper, **tool_kwargs ) diff --git a/examples/example-fastmcp-mcp/src/mcp.py b/examples/example-fastmcp-mcp/src/mcp.py deleted file mode 100644 index 12f14ac..0000000 --- a/examples/example-fastmcp-mcp/src/mcp.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -FastMCP server instance for Auth0 protected MCP server. -""" - -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP( - name="Auth0 Protected MCP Server", - stateless_http=True, -) diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py index 520f73a..7df521d 100644 --- a/examples/example-fastmcp-mcp/src/server.py +++ b/examples/example-fastmcp-mcp/src/server.py @@ -9,7 +9,7 @@ from starlette.routing import Mount from .auth0 import Auth0Mcp -from .mcp import mcp +from .tools import register_tools load_dotenv() @@ -22,14 +22,12 @@ audience=os.getenv("AUTH0_AUDIENCE"), domain=os.getenv("AUTH0_DOMAIN") ) +register_tools(auth0_mcp) @contextlib.asynccontextmanager async def lifespan(app: Starlette) -> AsyncIterator[None]: async with contextlib.AsyncExitStack() as stack: - await stack.enter_async_context(mcp.session_manager.run()) - - # Import tools here to ensure tools are loaded after mcp is initialized - from . import tools # noqa: F401, I001 + await stack.enter_async_context(auth0_mcp.mcp.session_manager.run()) yield starlette_app = Starlette( @@ -41,7 +39,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: # Main MCP app route with authentication middleware Mount( "/", - app=mcp.streamable_http_app(), + app=auth0_mcp.mcp.streamable_http_app(), middleware=auth0_mcp.auth_middleware() ), ], diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index 3d536e7..fa2f6a4 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -3,46 +3,50 @@ from mcp.server.fastmcp import Context from .auth0.tools import create_scoped_tool_decorator, get_auth_info -from .mcp import mcp -# Create a scoped_tool decorator bound to the mcp instance -scoped_tool = create_scoped_tool_decorator(mcp) +def register_tools(auth0Mcp): + """ + Register all tools with the MCP server. + """ + mcp = auth0Mcp.mcp + # Create a scoped_tool decorator bound to the mcp instance + scoped_tool = create_scoped_tool_decorator(auth0Mcp) -# Tool without required scopes -@mcp.tool() -def echo(text: str) -> str: - """Echoes the input text""" - return text + # Tool without required scopes + @mcp.tool() + def echo(text: str) -> str: + """Echoes the input text""" + return text -# A MCP tool with required scopes -@scoped_tool( - required_scopes=["tool:greet"], - name="greet", - title="Greet Tool", - description="Greets a user", - annotations={"readOnlyHint": True} -) -def greet(name: str, ctx: Context) -> str: - name = (name or "").strip() or "world" - request = ctx.request_context.request - auth_info = get_auth_info(request) - user_id = auth_info.get("extra", {}).get("sub") - return f"Hello, {name}! You are authenticated as {user_id}" + # A MCP tool with required scopes + @scoped_tool( + required_scopes=["tool:greet"], + name="greet", + title="Greet Tool", + description="Greets a user", + annotations={"readOnlyHint": True} + ) + def greet(name: str, ctx: Context) -> str: + name = (name or "").strip() or "world" + request = ctx.request_context.request + auth_info = get_auth_info(request) + user_id = auth_info.get("extra", {}).get("sub") + return f"Hello, {name}! You are authenticated as {user_id}" -# A MCP tool with required scopes -@scoped_tool( - required_scopes=["tool:whoami"], - name="whoami", - title="Who Am I Tool", - description="Returns information about the authenticated user", - annotations={"readOnlyHint": True} -) -def whoami(ctx: Context) -> str: - request = ctx.request_context.request - auth_info = get_auth_info(request) + # A MCP tool with required scopes + @scoped_tool( + required_scopes=["tool:whoami"], + name="whoami", + title="Who Am I Tool", + description="Returns information about the authenticated user", + annotations={"readOnlyHint": True} + ) + def whoami(ctx: Context) -> str: + request = ctx.request_context.request + auth_info = get_auth_info(request) - response_data = { - "user": auth_info.get("extra", {}), - "scopes": auth_info.get("scopes", []), - } - return jsonDumps(response_data, indent=2) + response_data = { + "user": auth_info.get("extra", {}), + "scopes": auth_info.get("scopes", []), + } + return jsonDumps(response_data, indent=2) From 4ae5d12df18df838f0a0d496104afccf2e7c391b Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:47:36 -0400 Subject: [PATCH 06/18] fix lint --- examples/example-fastmcp-mcp/src/auth0/__init__.py | 2 +- examples/example-fastmcp-mcp/src/tools.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 1bec722..7ee81a4 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -55,4 +55,4 @@ def register_scopes(self, scopes: list[str]) -> None: scopes: List of scopes to register (e.g., ["tool:greet", "tool:whoami"]) """ if scopes: - self._scopes_supported.update(scopes) \ No newline at end of file + self._scopes_supported.update(scopes) diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index fa2f6a4..fec89a8 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -4,6 +4,7 @@ from .auth0.tools import create_scoped_tool_decorator, get_auth_info + def register_tools(auth0Mcp): """ Register all tools with the MCP server. From 27f668abb41123ed3c4e4fb892cb10e08808eccd Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Sun, 28 Sep 2025 11:21:28 -0400 Subject: [PATCH 07/18] address feedback --- .../example-fastmcp-mcp/src/auth0/__init__.py | 61 ++++++++++++ .../example-fastmcp-mcp/src/auth0/authz.py | 46 +++++++++ .../example-fastmcp-mcp/src/auth0/errors.py | 47 +++++++++ .../src/auth0/middleware.py | 35 ++----- .../example-fastmcp-mcp/src/auth0/tools.py | 97 ------------------- examples/example-fastmcp-mcp/src/server.py | 1 + examples/example-fastmcp-mcp/src/tools.py | 20 ++-- 7 files changed, 171 insertions(+), 136 deletions(-) create mode 100644 examples/example-fastmcp-mcp/src/auth0/authz.py create mode 100644 examples/example-fastmcp-mcp/src/auth0/errors.py delete mode 100644 examples/example-fastmcp-mcp/src/auth0/tools.py diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 7ee81a4..23c5c9e 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -5,13 +5,22 @@ including token verification, middleware, and scoped tool decorators. """ +import logging +import os +from typing import Callable, Union + from mcp.server.auth.routes import create_protected_resource_routes from mcp.server.fastmcp import FastMCP from starlette.middleware import Middleware +from starlette.requests import Request +from starlette.responses import JSONResponse from starlette.routing import Route, Router +from .errors import AuthenticationRequired, InsufficientScope, MalformedAuthorizationRequest from .middleware import Auth0Middleware +logger = logging.getLogger(__name__) + class Auth0Mcp: def __init__(self, name: str, audience: str, domain: str): @@ -56,3 +65,55 @@ def register_scopes(self, scopes: list[str]) -> None: """ if scopes: self._scopes_supported.update(scopes) + + def exception_handlers(self) -> dict[Union[int, type[Exception]], Callable]: + return { + AuthenticationRequired: self._auth_error_handler, + InsufficientScope: self._auth_error_handler, + MalformedAuthorizationRequest: self._auth_error_handler, + # Generic fallback for any other exceptions + Exception: self._generic_exception_handler, + } + + def _auth_error_handler(self, request: Request, exc: Exception): + """ + Handle auth errors: malformed authorization requests, missing auth, invalid tokens, and insufficient scopes. + """ + # Include resource metadata parameter for 401 responses per RFC 9728 Section 5.1 + include_resource_metadata = exc.status_code == 401 + + return JSONResponse( + { + "error": exc.error_code, + "error_description": exc.description + }, + status_code=exc.status_code, + headers={"WWW-Authenticate": self._build_www_authenticate_header(exc.error_code, exc.description, include_resource_metadata)}, + ) + + def _generic_exception_handler(self, request:Request, exc: Exception): + """ + Fallback handler for all other exceptions. + """ + logger.error(f"Unexpected error in: {exc}", exc_info=exc) + + # Return standard HTTP 500 error + return JSONResponse( + { + "error": "internal_server_error", + "error_description": "An unexpected error occurred" + }, + status_code=500, + ) + + def _build_www_authenticate_header(self, error_code: str, description: str, include_resource_metadata: bool = False) -> str: + """ + Build WWW-Authenticate header according to RFC 9728 Section 5.1. + """ + www_auth_params = [f'error="{error_code}"', f'error_description="{description}"'] + + if include_resource_metadata: + metadata_url = f"{os.getenv('MCP_SERVER_URL')}/.well-known/oauth-protected-resource" + www_auth_params.append(f'resource_metadata="{metadata_url}"') + + return f"Bearer {', '.join(www_auth_params)}" diff --git a/examples/example-fastmcp-mcp/src/auth0/authz.py b/examples/example-fastmcp-mcp/src/auth0/authz.py new file mode 100644 index 0000000..98a5f2c --- /dev/null +++ b/examples/example-fastmcp-mcp/src/auth0/authz.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +from functools import wraps + +from mcp.server.fastmcp import Context + +from .errors import AuthenticationRequired, InsufficientScope + + +def require_scopes(required_scopes: Iterable[str]): + """ + Decorator that requires scopes on MCP tools. + + Example: + @mcp.tool(...) + @require_scopes(["tool:greet", "tool:whoami"]) + def my_tool(name: str, ctx: Context) -> str: + return f"Hello {name}!" + """ + required_scopes_list = list(required_scopes) + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # ctx is passed in either kw or positional + ctx: Context | None = (kwargs.get("ctx") if isinstance(kwargs.get("ctx"), Context) else None) or next((arg for arg in args if isinstance(arg, Context)), None) + if ctx is None: + raise TypeError("ctx: Context is required") + + auth = getattr(ctx.request_context.request.state, "auth", {}) + if not auth: + raise AuthenticationRequired("Authentication required") + + user_scopes = set(auth.get("scopes", [])) + missing_scopes = [s for s in required_scopes_list if s not in user_scopes] + if missing_scopes: + raise InsufficientScope(f"Missing required scopes: {missing_scopes}") + + # Call the original function + if asyncio.iscoroutinefunction(func): + return await func(*args, **kwargs) + else: + return func(*args, **kwargs) + return wrapper + return decorator diff --git a/examples/example-fastmcp-mcp/src/auth0/errors.py b/examples/example-fastmcp-mcp/src/auth0/errors.py new file mode 100644 index 0000000..622adb4 --- /dev/null +++ b/examples/example-fastmcp-mcp/src/auth0/errors.py @@ -0,0 +1,47 @@ +class AuthenticationRequired(Exception): + """ + Raised when authentication is required but missing. + + This maps to HTTP 401 Unauthorized status. + Indicates the request lacks valid authentication credentials. + """ + status_code = 401 + error_code = "invalid_token" + default_description = "Authentication required" + + def __init__(self, message: str | None = None): + self.description = message or self.default_description + super().__init__(self.description) + + +class InsufficientScope(Exception): + """ + Raised when user lacks required OAuth scopes. + + This maps to HTTP 403 Forbidden status. + Indicates the user is authenticated but doesn't have permission + to access the requested resource due to insufficient scopes. + """ + status_code = 403 + error_code = "insufficient_scope" + default_description = "Insufficient scope" + + def __init__(self, message: str | None = None): + self.description = message or self.default_description + super().__init__(self.description) + + +class MalformedAuthorizationRequest(Exception): + """ + Raised when authorization request is malformed. + + This maps to HTTP 400 Bad Request status. + Indicates the authorization header or token format is invalid. + """ + status_code = 400 + error_code = "invalid_request" + default_description = "Malformed authorization request" + + def __init__(self, message: str | None = None): + self.description = message or self.default_description + super().__init__(self.description) diff --git a/examples/example-fastmcp-mcp/src/auth0/middleware.py b/examples/example-fastmcp-mcp/src/auth0/middleware.py index 39f89ab..3c26002 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middleware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middleware.py @@ -1,13 +1,13 @@ import logging -import os from auth0_api_python import ApiClient, ApiClientOptions from auth0_api_python.errors import VerifyAccessTokenError from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request -from starlette.responses import JSONResponse from starlette.types import ASGIApp +from .errors import AuthenticationRequired, MalformedAuthorizationRequest + logger = logging.getLogger(__name__) class Auth0Middleware(BaseHTTPMiddleware): @@ -29,13 +29,9 @@ async def dispatch(self, request: Request, call_next): # Extract Authorization header auth_header = request.headers.get("authorization") if not auth_header: - return self._return_auth_error_response(status_code=401, error="Authentication required", description="Missing Authorization header") + raise AuthenticationRequired("Missing Authorization header") if not auth_header.lower().startswith("bearer "): - return self._return_auth_error_response( - status_code=401, - error="Authentication required", - description="Invalid Authorization header format" - ) + raise MalformedAuthorizationRequest("Invalid Authorization header format") # Extract and verify token token = auth_header[7:] # Remove "Bearer " prefix @@ -72,25 +68,8 @@ async def dispatch(self, request: Request, call_next): return await call_next(request) except VerifyAccessTokenError as e: logger.error(f"Token verification failed: {str(e)}") - return self._return_auth_error_response( - status_code=401, - error="Authentication failed", - description="Invalid token" - ) + raise AuthenticationRequired("Invalid token") except Exception as e: logger.error(f"Unexpected error in middleware: {str(e)}") - return self._return_auth_error_response( - status_code=500, - error="Internal Server Error", - description="Internal Server Error" - ) - - def _return_auth_error_response(self, status_code: int, error: str, description: str) -> JSONResponse: - www_auth_parts = [f'error="{error}"', f'error_description="{description}"', f'resource_metadata="{os.getenv("MCP_SERVER_URL")}"'] - www_authenticate = f"Bearer {', '.join(www_auth_parts)}" - - return JSONResponse( - status_code=status_code, - content={"error": error, "error_description": description}, - headers={"WWW-Authenticate": www_authenticate} - ) + # Re-raise unexpected errors to be handled by generic exception handler + raise diff --git a/examples/example-fastmcp-mcp/src/auth0/tools.py b/examples/example-fastmcp-mcp/src/auth0/tools.py deleted file mode 100644 index 45b096a..0000000 --- a/examples/example-fastmcp-mcp/src/auth0/tools.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Scope-based auth decorators for MCP tools. - -Provides a decorator with Auth0 scope checking for MCP tools. -""" -import asyncio -from functools import wraps -from typing import Callable - -from mcp.server.fastmcp import Context - - -def create_scoped_tool_decorator(auth0Mcp): - """Factory function to create a scoped_tool decorator bound to a MCP server instance.""" - - def scoped_tool( - required_scopes: list[str], - **tool_kwargs - ): - """ - Decorator that combines FastMCP tool registration with Auth0 scope checking. - - Args: - required_scopes: List of scopes required to use this tool - **tool_kwargs: Additional parameters passed to @mcp.tool() - - Example: - @scoped_tool(required_scopes=["read:data", "write:data"]) - def sensitive_tool(data: str, ctx: Context) -> str: - return f"Processing: {data}" - """ - - if required_scopes: - # register scopes for PRM - auth0Mcp.register_scopes(required_scopes) - - def decorator(func: Callable) -> Callable: - @wraps(func) - async def scope_checked_wrapper(*args, **kwargs): - # Find the Context parameter in kwargs - ctx = None - for value in kwargs.values(): - if isinstance(value, Context): - ctx = value - break - - if not ctx: - raise Exception(f"Tool '{func.__name__}' requires a Context parameter for scope checking") - - # Get auth info and check scopes - try: - request = ctx.request_context.request - auth_info = get_auth_info(request) - - if not auth_info or auth_info == {}: - raise Exception("Authentication required to use this tool") - - user_scopes = auth_info.get("scopes", []) - client_id = auth_info.get("client_id", "unknown") - - # Check if user has all required scopes - missing_scopes = [scope for scope in required_scopes if scope not in user_scopes] - if missing_scopes: - await ctx.error(f"Access denied: missing required scopes {missing_scopes}") - raise Exception( - f"Missing required scopes for tool '{func.__name__}': {missing_scopes}." - ) - - # Log successful scope check - await ctx.info(f"Tool '{func.__name__}' authorized for client '{client_id}' with scopes: {user_scopes}") - - except Exception as e: - # Log other unexpected errors and wrap them - await ctx.error(f"Authorization check failed for tool '{func.__name__}': {str(e)}") - raise Exception(f"Authorization check failed: {str(e)}") - - # Call the original function - if asyncio.iscoroutinefunction(func): - return await func(*args, **kwargs) - else: - return func(*args, **kwargs) - - # Register the wrapped function as an MCP tool - auth0Mcp.mcp.add_tool( - scope_checked_wrapper, - **tool_kwargs - ) - return scope_checked_wrapper - - return decorator - - return scoped_tool - - -def get_auth_info(request) -> dict: - """Get authentication info from request state.""" - return getattr(request.state, 'auth', {}) diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py index 7df521d..87aef78 100644 --- a/examples/example-fastmcp-mcp/src/server.py +++ b/examples/example-fastmcp-mcp/src/server.py @@ -44,6 +44,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: ), ], lifespan=lifespan, + exception_handlers=auth0_mcp.exception_handlers(), ) # Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index fec89a8..e39e5c0 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -2,7 +2,7 @@ from mcp.server.fastmcp import Context -from .auth0.tools import create_scoped_tool_decorator, get_auth_info +from .auth0.authz import require_scopes def register_tools(auth0Mcp): @@ -10,8 +10,8 @@ def register_tools(auth0Mcp): Register all tools with the MCP server. """ mcp = auth0Mcp.mcp - # Create a scoped_tool decorator bound to the mcp instance - scoped_tool = create_scoped_tool_decorator(auth0Mcp) + # Register scopes used by tools for Protected Resource Metadata + auth0Mcp.register_scopes(["tool:greet", "tool:whoami"]) # Tool without required scopes @mcp.tool() @@ -20,31 +20,29 @@ def echo(text: str) -> str: return text # A MCP tool with required scopes - @scoped_tool( - required_scopes=["tool:greet"], + @mcp.tool( name="greet", title="Greet Tool", description="Greets a user", annotations={"readOnlyHint": True} ) + @require_scopes(["tool:greet"]) def greet(name: str, ctx: Context) -> str: name = (name or "").strip() or "world" - request = ctx.request_context.request - auth_info = get_auth_info(request) + auth_info = ctx.request_context.request.state.auth user_id = auth_info.get("extra", {}).get("sub") return f"Hello, {name}! You are authenticated as {user_id}" # A MCP tool with required scopes - @scoped_tool( - required_scopes=["tool:whoami"], + @mcp.tool( name="whoami", title="Who Am I Tool", description="Returns information about the authenticated user", annotations={"readOnlyHint": True} ) + @require_scopes(["tool:whoami"]) def whoami(ctx: Context) -> str: - request = ctx.request_context.request - auth_info = get_auth_info(request) + auth_info = ctx.request_context.request.state.auth response_data = { "user": auth_info.get("extra", {}), From a5265cdac66cdaf11405f4a4fc0952203b027619 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:50:28 -0400 Subject: [PATCH 08/18] cleaning up --- .../example-fastmcp-mcp/src/auth0/__init__.py | 10 ++++---- .../example-fastmcp-mcp/src/auth0/authz.py | 9 ++------ .../example-fastmcp-mcp/src/auth0/errors.py | 3 +++ .../src/auth0/middleware.py | 23 +++++++++---------- examples/example-fastmcp-mcp/src/server.py | 2 ++ examples/example-fastmcp-mcp/src/tools.py | 10 ++++---- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 23c5c9e..0c23b2c 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -5,6 +5,8 @@ including token verification, middleware, and scoped tool decorators. """ +from __future__ import annotations + import logging import os from typing import Callable, Union @@ -30,7 +32,7 @@ def __init__(self, name: str, audience: str, domain: str): if not self.audience or not self.domain: raise RuntimeError("audience and domain must be provided") self.mcp = FastMCP( - name="Auth0 Protected MCP Server", + name=self.name, stateless_http=True, ) self._scopes_supported = { @@ -111,9 +113,9 @@ def _build_www_authenticate_header(self, error_code: str, description: str, incl Build WWW-Authenticate header according to RFC 9728 Section 5.1. """ www_auth_params = [f'error="{error_code}"', f'error_description="{description}"'] - - if include_resource_metadata: - metadata_url = f"{os.getenv('MCP_SERVER_URL')}/.well-known/oauth-protected-resource" + metadata_url = os.getenv('MCP_SERVER_URL') + if include_resource_metadata and metadata_url: + metadata_url = metadata_url.rstrip("/") + "/.well-known/oauth-protected-resource" www_auth_params.append(f'resource_metadata="{metadata_url}"') return f"Bearer {', '.join(www_auth_params)}" diff --git a/examples/example-fastmcp-mcp/src/auth0/authz.py b/examples/example-fastmcp-mcp/src/auth0/authz.py index 98a5f2c..ee1e410 100644 --- a/examples/example-fastmcp-mcp/src/auth0/authz.py +++ b/examples/example-fastmcp-mcp/src/auth0/authz.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio from collections.abc import Iterable from functools import wraps @@ -16,7 +15,7 @@ def require_scopes(required_scopes: Iterable[str]): Example: @mcp.tool(...) @require_scopes(["tool:greet", "tool:whoami"]) - def my_tool(name: str, ctx: Context) -> str: + async def my_tool(name: str, ctx: Context) -> str: return f"Hello {name}!" """ required_scopes_list = list(required_scopes) @@ -37,10 +36,6 @@ async def wrapper(*args, **kwargs): if missing_scopes: raise InsufficientScope(f"Missing required scopes: {missing_scopes}") - # Call the original function - if asyncio.iscoroutinefunction(func): - return await func(*args, **kwargs) - else: - return func(*args, **kwargs) + return await func(*args, **kwargs) return wrapper return decorator diff --git a/examples/example-fastmcp-mcp/src/auth0/errors.py b/examples/example-fastmcp-mcp/src/auth0/errors.py index 622adb4..ef0644f 100644 --- a/examples/example-fastmcp-mcp/src/auth0/errors.py +++ b/examples/example-fastmcp-mcp/src/auth0/errors.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class AuthenticationRequired(Exception): """ Raised when authentication is required but missing. diff --git a/examples/example-fastmcp-mcp/src/auth0/middleware.py b/examples/example-fastmcp-mcp/src/auth0/middleware.py index 3c26002..4f2bad1 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middleware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middleware.py @@ -29,12 +29,12 @@ async def dispatch(self, request: Request, call_next): # Extract Authorization header auth_header = request.headers.get("authorization") if not auth_header: - raise AuthenticationRequired("Missing Authorization header") + raise MalformedAuthorizationRequest("Missing Authorization header") if not auth_header.lower().startswith("bearer "): raise MalformedAuthorizationRequest("Invalid Authorization header format") # Extract and verify token - token = auth_header[7:] # Remove "Bearer " prefix + token = auth_header[7:].strip() # Remove "Bearer " prefix try: decoded_and_verified_token = await self.client.verify_access_token( token, @@ -42,21 +42,21 @@ async def dispatch(self, request: Request, call_next): ) # Check for client_id or azp - clientId = decoded_and_verified_token.get('client_id') or decoded_and_verified_token.get('azp') - if not clientId: + client_id = decoded_and_verified_token.get('client_id') or decoded_and_verified_token.get('azp') + if not client_id: raise VerifyAccessTokenError("Token is missing 'client_id' or 'azp' claim") # Set up authentication context auth_data = { - "client_id": clientId, + "client_id": client_id, "scopes": decoded_and_verified_token.get("scope", "").split() if decoded_and_verified_token.get("scope") else [] } if decoded_and_verified_token.get('exp'): - auth_data["expiresAt"] = decoded_and_verified_token.get('exp') + auth_data["expires_at"] = decoded_and_verified_token.get('exp') - extra = {"sub": decoded_and_verified_token.get('sub'), "client_id": clientId} + extra = {"sub": decoded_and_verified_token.get('sub'), "client_id": client_id} for field in ['azp', 'name', 'email']: if decoded_and_verified_token.get(field): @@ -66,10 +66,9 @@ async def dispatch(self, request: Request, call_next): request.state.auth = auth_data return await call_next(request) - except VerifyAccessTokenError as e: - logger.error(f"Token verification failed: {str(e)}") + except VerifyAccessTokenError: + logger.info("Token verification failed") raise AuthenticationRequired("Invalid token") - except Exception as e: - logger.error(f"Unexpected error in middleware: {str(e)}") - # Re-raise unexpected errors to be handled by generic exception handler + except Exception: + logger.exception("Unexpected error in middleware") raise diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py index 87aef78..519a6df 100644 --- a/examples/example-fastmcp-mcp/src/server.py +++ b/examples/example-fastmcp-mcp/src/server.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import logging import os diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index e39e5c0..4ca6692 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -1,4 +1,4 @@ -from json import dumps as jsonDumps +import json from mcp.server.fastmcp import Context @@ -15,7 +15,7 @@ def register_tools(auth0Mcp): # Tool without required scopes @mcp.tool() - def echo(text: str) -> str: + async def echo(text: str) -> str: """Echoes the input text""" return text @@ -27,7 +27,7 @@ def echo(text: str) -> str: annotations={"readOnlyHint": True} ) @require_scopes(["tool:greet"]) - def greet(name: str, ctx: Context) -> str: + async def greet(name: str, ctx: Context) -> str: name = (name or "").strip() or "world" auth_info = ctx.request_context.request.state.auth user_id = auth_info.get("extra", {}).get("sub") @@ -41,11 +41,11 @@ def greet(name: str, ctx: Context) -> str: annotations={"readOnlyHint": True} ) @require_scopes(["tool:whoami"]) - def whoami(ctx: Context) -> str: + async def whoami(ctx: Context) -> str: auth_info = ctx.request_context.request.state.auth response_data = { "user": auth_info.get("extra", {}), "scopes": auth_info.get("scopes", []), } - return jsonDumps(response_data, indent=2) + return json.dumps(response_data, indent=2) From 24f9bcfda21a48d4d3a2550e47d63bcfb8bbf6f9 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:09:45 -0400 Subject: [PATCH 09/18] address feedback --- .../example-fastmcp-mcp/src/auth0/__init__.py | 6 +-- .../src/auth0/middleware.py | 54 +++++++++++-------- examples/example-fastmcp-mcp/src/tools.py | 4 +- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 0c23b2c..307f10c 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -9,13 +9,13 @@ import logging import os -from typing import Callable, Union +from collections.abc import Callable from mcp.server.auth.routes import create_protected_resource_routes from mcp.server.fastmcp import FastMCP from starlette.middleware import Middleware from starlette.requests import Request -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response from starlette.routing import Route, Router from .errors import AuthenticationRequired, InsufficientScope, MalformedAuthorizationRequest @@ -68,7 +68,7 @@ def register_scopes(self, scopes: list[str]) -> None: if scopes: self._scopes_supported.update(scopes) - def exception_handlers(self) -> dict[Union[int, type[Exception]], Callable]: + def exception_handlers(self) -> dict[int | type[Exception], Callable[[Request, Exception], Response]]: return { AuthenticationRequired: self._auth_error_handler, InsufficientScope: self._auth_error_handler, diff --git a/examples/example-fastmcp-mcp/src/auth0/middleware.py b/examples/example-fastmcp-mcp/src/auth0/middleware.py index 4f2bad1..c47dbef 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middleware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middleware.py @@ -1,9 +1,12 @@ import logging +from collections.abc import Callable +from typing import Any from auth0_api_python import ApiClient, ApiClientOptions from auth0_api_python.errors import VerifyAccessTokenError from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request +from starlette.responses import Response from starlette.types import ASGIApp from .errors import AuthenticationRequired, MalformedAuthorizationRequest @@ -25,7 +28,33 @@ def __init__(self, app: ASGIApp, domain: str, audience: str): audience=audience )) - async def dispatch(self, request: Request, call_next): + def _build_auth_data(self, token: dict[str, Any]) -> dict[str, Any]: + """Extract authentication data from verified token.""" + client_id = token.get('client_id') or token.get('azp') + if not client_id: + raise VerifyAccessTokenError("Token missing 'client_id' or 'azp' claim") + + scopes = token.get("scope", "").split() if token.get("scope") else [] + + auth_data = { + "client_id": client_id, + "scopes": scopes, + } + + if expires_at := token.get('exp'): + auth_data["expires_at"] = expires_at + + # Extract extra claims with dict comprehension + extra_fields = {'sub', 'azp', 'name', 'email', 'client_id'} + auth_data["extra"] = { + field: token[field] + for field in extra_fields + if field in token + } + + return auth_data + + async def dispatch(self, request: Request, call_next: Callable) -> Response: # Extract Authorization header auth_header = request.headers.get("authorization") if not auth_header: @@ -41,29 +70,8 @@ async def dispatch(self, request: Request, call_next): required_claims=["sub"] ) - # Check for client_id or azp - client_id = decoded_and_verified_token.get('client_id') or decoded_and_verified_token.get('azp') - if not client_id: - raise VerifyAccessTokenError("Token is missing 'client_id' or 'azp' claim") - # Set up authentication context - auth_data = { - "client_id": client_id, - "scopes": decoded_and_verified_token.get("scope", "").split() - if decoded_and_verified_token.get("scope") else [] - } - - if decoded_and_verified_token.get('exp'): - auth_data["expires_at"] = decoded_and_verified_token.get('exp') - - extra = {"sub": decoded_and_verified_token.get('sub'), "client_id": client_id} - - for field in ['azp', 'name', 'email']: - if decoded_and_verified_token.get(field): - extra[field] = decoded_and_verified_token.get(field) - - auth_data["extra"] = extra - request.state.auth = auth_data + request.state.auth = self._build_auth_data(decoded_and_verified_token) return await call_next(request) except VerifyAccessTokenError: diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index 4ca6692..7aa5622 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -5,7 +5,7 @@ from .auth0.authz import require_scopes -def register_tools(auth0Mcp): +def register_tools(auth0Mcp) -> None: """ Register all tools with the MCP server. """ @@ -28,7 +28,7 @@ async def echo(text: str) -> str: ) @require_scopes(["tool:greet"]) async def greet(name: str, ctx: Context) -> str: - name = (name or "").strip() or "world" + name = name.strip() if name else "world" auth_info = ctx.request_context.request.state.auth user_id = auth_info.get("extra", {}).get("sub") return f"Hello, {name}! You are authenticated as {user_id}" From 534f05d0b50afaecb47a1a0bc1912f4201fd3686 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:10:30 -0400 Subject: [PATCH 10/18] fix lint error --- examples/example-fastmcp-mcp/src/auth0/middleware.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/middleware.py b/examples/example-fastmcp-mcp/src/auth0/middleware.py index c47dbef..c3f3377 100644 --- a/examples/example-fastmcp-mcp/src/auth0/middleware.py +++ b/examples/example-fastmcp-mcp/src/auth0/middleware.py @@ -33,17 +33,17 @@ def _build_auth_data(self, token: dict[str, Any]) -> dict[str, Any]: client_id = token.get('client_id') or token.get('azp') if not client_id: raise VerifyAccessTokenError("Token missing 'client_id' or 'azp' claim") - + scopes = token.get("scope", "").split() if token.get("scope") else [] - + auth_data = { "client_id": client_id, "scopes": scopes, } - + if expires_at := token.get('exp'): auth_data["expires_at"] = expires_at - + # Extract extra claims with dict comprehension extra_fields = {'sub', 'azp', 'name', 'email', 'client_id'} auth_data["extra"] = { @@ -51,7 +51,7 @@ def _build_auth_data(self, token: dict[str, Any]) -> dict[str, Any]: for field in extra_fields if field in token } - + return auth_data async def dispatch(self, request: Request, call_next: Callable) -> Response: From af2d985c8438d51860ac3ec44a41c034861fda21 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:06:47 -0400 Subject: [PATCH 11/18] add config.py --- examples/example-fastmcp-mcp/.env.example | 3 +- .../example-fastmcp-mcp/src/auth0/__init__.py | 9 +++-- examples/example-fastmcp-mcp/src/config.py | 33 +++++++++++++++++++ examples/example-fastmcp-mcp/src/server.py | 17 +++++----- 4 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 examples/example-fastmcp-mcp/src/config.py diff --git a/examples/example-fastmcp-mcp/.env.example b/examples/example-fastmcp-mcp/.env.example index de14826..71a4aa8 100644 --- a/examples/example-fastmcp-mcp/.env.example +++ b/examples/example-fastmcp-mcp/.env.example @@ -3,4 +3,5 @@ AUTH0_DOMAIN=your-tenant.auth0.com AUTH0_AUDIENCE=https://api.example.com MCP_SERVER_URL=http://localhost:3001 PORT=3001 -DEBUG=false \ No newline at end of file +DEBUG=false +CORS_ORIGINS=* \ No newline at end of file diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 307f10c..8441e3f 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -8,7 +8,6 @@ from __future__ import annotations import logging -import os from collections.abc import Callable from mcp.server.auth.routes import create_protected_resource_routes @@ -25,10 +24,11 @@ class Auth0Mcp: - def __init__(self, name: str, audience: str, domain: str): + def __init__(self, name: str, audience: str, domain: str, mcp_server_url: str | None = None): self.name = name self.audience = audience self.domain = domain + self.mcp_server_url = mcp_server_url if not self.audience or not self.domain: raise RuntimeError("audience and domain must be provided") self.mcp = FastMCP( @@ -113,9 +113,8 @@ def _build_www_authenticate_header(self, error_code: str, description: str, incl Build WWW-Authenticate header according to RFC 9728 Section 5.1. """ www_auth_params = [f'error="{error_code}"', f'error_description="{description}"'] - metadata_url = os.getenv('MCP_SERVER_URL') - if include_resource_metadata and metadata_url: - metadata_url = metadata_url.rstrip("/") + "/.well-known/oauth-protected-resource" + if include_resource_metadata and self.mcp_server_url: + metadata_url = self.mcp_server_url.rstrip("/") + "/.well-known/oauth-protected-resource" www_auth_params.append(f'resource_metadata="{metadata_url}"') return f"Bearer {', '.join(www_auth_params)}" diff --git a/examples/example-fastmcp-mcp/src/config.py b/examples/example-fastmcp-mcp/src/config.py new file mode 100644 index 0000000..0ebea10 --- /dev/null +++ b/examples/example-fastmcp-mcp/src/config.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass, field + +from dotenv import load_dotenv + + +@dataclass(frozen=True) +class Config: + auth0_domain: str + auth0_audience: str + mcp_server_url: str + port: int = 3001 + debug: bool = True + cors_origins: list[str] = field(default_factory=lambda: ["*"]) + + @classmethod + def from_env(cls) -> Config: + return cls( + auth0_domain=os.environ["AUTH0_DOMAIN"], + auth0_audience=os.environ["AUTH0_AUDIENCE"], + mcp_server_url=os.getenv("MCP_SERVER_URL", "http://localhost:3001"), + port=int(os.getenv("PORT", "3001")), + debug=os.getenv("DEBUG", "false").lower() == "true", + cors_origins=os.getenv("CORS_ORIGINS", "*").split(","), + ) + + +def get_config() -> Config: + """Get application configuration.""" + load_dotenv() + return Config.from_env() diff --git a/examples/example-fastmcp-mcp/src/server.py b/examples/example-fastmcp-mcp/src/server.py index 519a6df..1793ec3 100644 --- a/examples/example-fastmcp-mcp/src/server.py +++ b/examples/example-fastmcp-mcp/src/server.py @@ -2,18 +2,17 @@ import contextlib import logging -import os from collections.abc import AsyncIterator -from dotenv import load_dotenv from starlette.applications import Starlette from starlette.middleware.cors import CORSMiddleware from starlette.routing import Mount from .auth0 import Auth0Mcp +from .config import get_config from .tools import register_tools -load_dotenv() +config = get_config() # Configure logging logging.basicConfig(level=logging.INFO) @@ -21,8 +20,9 @@ auth0_mcp = Auth0Mcp( name="Example FastMCP Server", - audience=os.getenv("AUTH0_AUDIENCE"), - domain=os.getenv("AUTH0_DOMAIN") + audience=config.auth0_audience, + domain=config.auth0_domain, + mcp_server_url=config.mcp_server_url ) register_tools(auth0_mcp) @@ -33,7 +33,7 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: yield starlette_app = Starlette( - debug=os.getenv("DEBUG", "true").lower() == "true", + debug=config.debug, routes=[ # Add discovery metadata route *auth0_mcp.auth_metadata_router().routes, @@ -51,13 +51,14 @@ async def lifespan(app: Starlette) -> AsyncIterator[None]: # Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header # for browser-based clients (ensures 500 errors get proper CORS headers) + app = CORSMiddleware( starlette_app, - allow_origins=["*"], # Adjust as needed for production + allow_origins=config.cors_origins, allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods expose_headers=["Mcp-Session-Id"], ) if __name__ == "__main__": import uvicorn - uvicorn.run(app, port=int(os.getenv("PORT", "3001"))) + uvicorn.run(app, port=config.port) From 059b42b59f7d83d9b3ec15dd090774f8a7109ce8 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:24:24 -0400 Subject: [PATCH 12/18] more typings and add docstrings --- .../example-fastmcp-mcp/src/auth0/__init__.py | 16 +++++++++++++++- examples/example-fastmcp-mcp/src/tools.py | 7 ++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 8441e3f..155d11f 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -24,6 +24,20 @@ class Auth0Mcp: + """ + Auth0 integration for FastMCP servers. + + Provides authentication middleware, authorization decorators, + and OAuth2 Protected Resource metadata endpoints for MCP servers. + + Args: + name: Human-readable name for the MCP server + audience: Auth0 API identifier (OAuth2 audience claim) + domain: Auth0 tenant domain (e.g., 'tenant.us.auth0.com') + + Raises: + RuntimeError: If audience or domain are not provided + """ def __init__(self, name: str, audience: str, domain: str, mcp_server_url: str | None = None): self.name = name self.audience = audience @@ -77,7 +91,7 @@ def exception_handlers(self) -> dict[int | type[Exception], Callable[[Request, E Exception: self._generic_exception_handler, } - def _auth_error_handler(self, request: Request, exc: Exception): + def _auth_error_handler(self, request: Request, exc: Exception) -> JSONResponse: """ Handle auth errors: malformed authorization requests, missing auth, invalid tokens, and insufficient scopes. """ diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index 7aa5622..57a2b89 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -2,16 +2,17 @@ from mcp.server.fastmcp import Context +from .auth0 import Auth0Mcp from .auth0.authz import require_scopes -def register_tools(auth0Mcp) -> None: +def register_tools(auth0_mcp: Auth0Mcp) -> None: """ Register all tools with the MCP server. """ - mcp = auth0Mcp.mcp + mcp = auth0_mcp.mcp # Register scopes used by tools for Protected Resource Metadata - auth0Mcp.register_scopes(["tool:greet", "tool:whoami"]) + auth0_mcp.register_scopes(["tool:greet", "tool:whoami"]) # Tool without required scopes @mcp.tool() From dc8d056a4e07c78b7bd1c8235c9826407fafa5e1 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:31:33 -0400 Subject: [PATCH 13/18] fix space issue --- examples/example-fastmcp-mcp/src/auth0/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/__init__.py b/examples/example-fastmcp-mcp/src/auth0/__init__.py index 155d11f..0ccf646 100644 --- a/examples/example-fastmcp-mcp/src/auth0/__init__.py +++ b/examples/example-fastmcp-mcp/src/auth0/__init__.py @@ -91,7 +91,7 @@ def exception_handlers(self) -> dict[int | type[Exception], Callable[[Request, E Exception: self._generic_exception_handler, } - def _auth_error_handler(self, request: Request, exc: Exception) -> JSONResponse: + def _auth_error_handler(self, request: Request, exc: Exception) -> JSONResponse: """ Handle auth errors: malformed authorization requests, missing auth, invalid tokens, and insufficient scopes. """ @@ -107,7 +107,7 @@ def _auth_error_handler(self, request: Request, exc: Exception) -> JSONResponse headers={"WWW-Authenticate": self._build_www_authenticate_header(exc.error_code, exc.description, include_resource_metadata)}, ) - def _generic_exception_handler(self, request:Request, exc: Exception): + def _generic_exception_handler(self, request: Request, exc: Exception) -> JSONResponse: """ Fallback handler for all other exceptions. """ From 8b93b993b00428e38e039e35104a1069576ee52b Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:12:10 -0400 Subject: [PATCH 14/18] Register scopes automatically for PRM --- examples/example-fastmcp-mcp/src/auth0/authz.py | 13 +++++++++++++ examples/example-fastmcp-mcp/src/tools.py | 7 ++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/auth0/authz.py b/examples/example-fastmcp-mcp/src/auth0/authz.py index ee1e410..9bce8b4 100644 --- a/examples/example-fastmcp-mcp/src/auth0/authz.py +++ b/examples/example-fastmcp-mcp/src/auth0/authz.py @@ -5,8 +5,11 @@ from mcp.server.fastmcp import Context +from . import Auth0Mcp from .errors import AuthenticationRequired, InsufficientScope +# Collect required scopes from all decorated functions +_scopes_required: set[str] = set() def require_scopes(required_scopes: Iterable[str]): """ @@ -19,6 +22,10 @@ async def my_tool(name: str, ctx: Context) -> str: return f"Hello {name}!" """ required_scopes_list = list(required_scopes) + + # Collect scopes when decorator is applied + _scopes_required.update(required_scopes_list) + def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): @@ -39,3 +46,9 @@ async def wrapper(*args, **kwargs): return await func(*args, **kwargs) return wrapper return decorator + +def register_required_scopes(auth0_mcp: Auth0Mcp) -> None: + """Register all scopes that were collected from @require_scopes decorators.""" + if _scopes_required: + auth0_mcp.register_scopes(list(_scopes_required)) + _scopes_required.clear() diff --git a/examples/example-fastmcp-mcp/src/tools.py b/examples/example-fastmcp-mcp/src/tools.py index 57a2b89..3b3f2e7 100644 --- a/examples/example-fastmcp-mcp/src/tools.py +++ b/examples/example-fastmcp-mcp/src/tools.py @@ -3,7 +3,7 @@ from mcp.server.fastmcp import Context from .auth0 import Auth0Mcp -from .auth0.authz import require_scopes +from .auth0.authz import register_required_scopes, require_scopes def register_tools(auth0_mcp: Auth0Mcp) -> None: @@ -11,8 +11,6 @@ def register_tools(auth0_mcp: Auth0Mcp) -> None: Register all tools with the MCP server. """ mcp = auth0_mcp.mcp - # Register scopes used by tools for Protected Resource Metadata - auth0_mcp.register_scopes(["tool:greet", "tool:whoami"]) # Tool without required scopes @mcp.tool() @@ -50,3 +48,6 @@ async def whoami(ctx: Context) -> str: "scopes": auth_info.get("scopes", []), } return json.dumps(response_data, indent=2) + + # Register all scopes used by tools for Protected Resource Metadata + register_required_scopes(auth0_mcp) From fb009199c8910f6a0b9214b22f7f1422e375d059 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:13:16 -0400 Subject: [PATCH 15/18] fail fast when required envs are missing --- examples/example-fastmcp-mcp/src/config.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/example-fastmcp-mcp/src/config.py b/examples/example-fastmcp-mcp/src/config.py index 0ebea10..edb3cb6 100644 --- a/examples/example-fastmcp-mcp/src/config.py +++ b/examples/example-fastmcp-mcp/src/config.py @@ -17,9 +17,17 @@ class Config: @classmethod def from_env(cls) -> Config: + auth0_domain = os.getenv("AUTH0_DOMAIN") + if not auth0_domain: + raise ValueError("AUTH0_DOMAIN environment variable is required") + + auth0_audience = os.getenv("AUTH0_AUDIENCE") + if not auth0_audience: + raise ValueError("AUTH0_AUDIENCE environment variable is required") + return cls( - auth0_domain=os.environ["AUTH0_DOMAIN"], - auth0_audience=os.environ["AUTH0_AUDIENCE"], + auth0_domain=auth0_domain, + auth0_audience=auth0_audience, mcp_server_url=os.getenv("MCP_SERVER_URL", "http://localhost:3001"), port=int(os.getenv("PORT", "3001")), debug=os.getenv("DEBUG", "false").lower() == "true", From 3211d73bdec7095cea635411b32d42c33ba8efc1 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:50:52 -0400 Subject: [PATCH 16/18] add comments to .env.example --- examples/example-fastmcp-mcp/.env.example | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/examples/example-fastmcp-mcp/.env.example b/examples/example-fastmcp-mcp/.env.example index 71a4aa8..ede3ba1 100644 --- a/examples/example-fastmcp-mcp/.env.example +++ b/examples/example-fastmcp-mcp/.env.example @@ -1,7 +1,19 @@ # Auth0 Configuration + +# Auth0 tenant domain (e.g., your-tenant.us.auth0.com) AUTH0_DOMAIN=your-tenant.auth0.com + +# Auth0 API Identifier - must match the audience in your Auth0 API configuration AUTH0_AUDIENCE=https://api.example.com + +# URL where this MCP server is accessible (used for OAuth metadata) MCP_SERVER_URL=http://localhost:3001 + +# Port the server will listen on PORT=3001 + +# Enable debug mode for detailed logging DEBUG=false + +# CORS origins - comma-separated list of allowed origins (* for all) CORS_ORIGINS=* \ No newline at end of file From d3f2cd2c4f6ffee0b56337045ad42d4fbe10a237 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:41:14 -0400 Subject: [PATCH 17/18] adds auth0 tenant setup to README --- examples/example-fastmcp-mcp/README.md | 120 ++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/examples/example-fastmcp-mcp/README.md b/examples/example-fastmcp-mcp/README.md index faeb9c5..d3972ee 100644 --- a/examples/example-fastmcp-mcp/README.md +++ b/examples/example-fastmcp-mcp/README.md @@ -10,7 +10,125 @@ poetry install ## Auth0 Tenant Setup -For detailed instructions on setting up your Auth0 tenant for MCP server integration, please refer to the [Auth0 Tenant Setup guide](https://github.com/auth0/auth0-auth-js/blob/main/examples/example-fastmcp-mcp/README.md#auth0-tenant-setup). +### Pre-requisites: + +This guide uses [Auth0 CLI](https://auth0.github.io/auth0-cli/) to configure an Auth0 tenant for secure MCP tool access. If you don't have it, you can follow the [Auth0 CLI installation instructions](https://auth0.github.io/auth0-cli/) to set it up. Alternatively, all the following configuration steps can be done through the [Auth0 Management Dashboard](https://manage.auth0.com/). + +### Step 1: Authenticate with Auth0 CLI + +First, you need to log in to the Auth0 CLI with the correct scopes to manage all the necessary resources. + +1. Run the login command: This command will open a browser window for you to authenticate. We are requesting a set of + scopes to configure APIs, roles, and clients. + +``` +auth0 login --scopes "read:client_grants,create:client_grants,delete:client_grants,read:clients,create:clients,update:clients,read:resource_servers,create:resource_servers,update:resource_servers,read:roles,create:roles,update:roles,update:tenant_settings,read:connections,update:connections" +``` + +2. Verify your tenant: After logging in, confirm you are operating on the tenant you want to configure. + +``` +auth0 tenants list +``` + +### Step 2: Configure Tenant Settings + +Next, enable tenant-level flags required for Dynamic Client Registration (DCR) and an improved user consent experience. + +- `enable_dynamic_client_registration`: Allows MCP tools to register themselves as applications automatically. + [Learn more](https://auth0.com/docs/get-started/applications/dynamic-client-registration#enable-dynamic-client-registration) +- `use_scope_descriptions_for_consent`: Shows user-friendly descriptions for scopes on the consent screen. + [Learn more](https://auth0.com/docs/customize/login-pages/customize-consent-prompts). + +Execute the following command to enable the above mentioned flags through the tenant settings: + +``` +auth0 tenant-settings update set flags.enable_dynamic_client_registration flags.use_scope_descriptions_for_consent +``` + +### Step 3: Promote Connections to Domain Level + +[Learn more](https://auth0.com/docs/authenticate/identity-providers/promote-connections-to-domain-level) about promoting +connections to domain level. + +1. List your connections to get their IDs: `auth0 api get connections` +2. From the list, identify only the connections that should be available to be used with third party applications. For each of those specific connection IDs, run the following command to mark it as a domain-level connection. Replace `YOUR_CONNECTION_ID` with the actual ID (e.g., `con_XXXXXXXXXXXXXXXX`) + +``` +auth0 api patch connections/YOUR_CONNECTION_ID --data '{"is_domain_connection": true}' +``` + +### Step 4: Configure the API and Default Audience + +This step creates the API (also known as a Resource Server) that represents your protected MCP Server and sets it as the +default for your tenant. + +1. Create the API: This command registers the API with Auth0, defines its signing algorithm, enables Role-Based Access + Control (RBAC), and specifies the available scopes. Replace `http://localhost:3001` and `MCP Tools API` + with your desired identifier and name. Add your tool-specific scopes to the scopes array. + + Note that `rfc9068_profile_authz` is used instead of `rfc9068_profile` as the token dialect to enable RBAC. [Learn more](https://auth0.com/docs/get-started/apis/enable-role-based-access-control-for-apis#token-dialect-options) + +``` +auth0 api post resource-servers --data '{ + "identifier": "http://localhost:3001", + "name": "MCP Tools API", + "signing_alg": "RS256", + "token_dialect": "rfc9068_profile_authz", + "enforce_policies": true, + "scopes": [ + {"value": "tool:whoami", "description": "Access the WhoAmI tool"}, + {"value": "tool:greet", "description": "Access the Greeting tool"} + ] +}' + +``` + +2. Set the Default Audience: This ensures that users logging in interactively get access tokens that are valid for your + newly created MCP Server. Replace `http://localhost:3001` with the same API identifier you used above. + + **Note:** This step is currently required but temporary. Without setting a default audience, the issued access tokens will not be scoped specifically to your MCP resource server. Support for RFC 8707 (Resource Indicators for OAuth 2.0) is coming soon, which will provide proper resource targeting. Once available, these instructions will be updated to explain how to enable support for RFC 8707 instead of the default audience approach. + +``` +auth0 api patch "tenants/settings" --data '{"default_audience": "http://localhost:3001"}' +``` + +### Step 5: Configure RBAC Roles and Permissions + +Now, set up roles and assign permissions to them. This allows you to control which users can access which tools. + +1. Create Roles: For each role you need (e.g., "Tool Administrator", "Tool User"), run the create command. + +``` +# Example for an admin role +auth0 roles create --name "Tool Administrator" --description "Grants access to all MCP tools" + +# Example for a basic user role +auth0 roles create --name "Tool User" --description "Grants access to basic MCP tools" +``` + +2. Assign Permissions to Roles: After creating roles, note the ID from the output (e.g. `rol_`) and and assign the API + permissions to it. Replace `YOUR_ROLE_ID`, `http://localhost:3001`, and the list of scopes. + +``` +# Example for admin role (all scopes) +auth0 roles permissions add YOUR_ADMIN_ROLE_ID --api-id "http://localhost:3001" --permissions "tool:whoami,tool:greet" + +# Example for user role (one scope) +auth0 roles permissions add YOUR_USER_ROLE_ID --api-id "http://localhost:3001" --permissions "tool:whoami" +``` + +3. Assign Roles to Users: Find users and assign them to the roles. + +``` +# Find a user's ID +auth0 users search --query "email:\"example@google.com\"" + +# Assign the role using the user's ID and the role's ID +auth0 users roles assign "auth0|USER_ID_HERE" --roles "YOUR_ROLE_ID_HERE" +``` + +**Note:** Further customization not supported out of the box by RBAC can be done via a custom Post-Login action trigger. ## Configuration From 6796a80413304ea3ff947f493946788e18f809e4 Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:23:06 -0400 Subject: [PATCH 18/18] adds some curl commands in testing --- examples/example-fastmcp-mcp/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/example-fastmcp-mcp/README.md b/examples/example-fastmcp-mcp/README.md index d3972ee..8d07694 100644 --- a/examples/example-fastmcp-mcp/README.md +++ b/examples/example-fastmcp-mcp/README.md @@ -159,3 +159,20 @@ npx @modelcontextprotocol/inspector The server will start up and the UI will be accessible at http://localhost:6274. In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL. + +### Using cURL + +You can use cURL to verify that the server is running: + +```bash +# Test that the server is running and accessible - check OAuth resource metadata +curl -v http://localhost:3001/.well-known/oauth-protected-resource + +# Test MCP initialization (requires valid Auth0 access token) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "curl-test", "version": "1.0.0"}}}' +``` + +**Note:** Use the MCP Inspector or other MCP-compatible clients for comprehensive testing.