From d38c0c059a87714d33174bb7ba5e78b897840583 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 13:31:35 -0400 Subject: [PATCH 01/42] deps: add tomli>=2.3.0 dependency to UnityMcpServer package --- .../UnityMcpServer~/src/pyproject.toml | 6 +- MCPForUnity/UnityMcpServer~/src/uv.lock | 572 ++++++++++++++---- 2 files changed, 450 insertions(+), 128 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/pyproject.toml b/MCPForUnity/UnityMcpServer~/src/pyproject.toml index ca02d6e3..76137d9e 100644 --- a/MCPForUnity/UnityMcpServer~/src/pyproject.toml +++ b/MCPForUnity/UnityMcpServer~/src/pyproject.toml @@ -4,7 +4,11 @@ version = "6.0.0" description = "MCP for Unity Server: A Unity package for Unity Editor integration via the Model Context Protocol (MCP)." readme = "README.md" requires-python = ">=3.10" -dependencies = ["httpx>=0.27.2", "mcp[cli]>=1.15.0"] +dependencies = [ + "httpx>=0.27.2", + "mcp[cli]>=1.15.0", + "tomli>=2.3.0", +] [build-system] requires = ["setuptools>=64.0.0", "wheel"] diff --git a/MCPForUnity/UnityMcpServer~/src/uv.lock b/MCPForUnity/UnityMcpServer~/src/uv.lock index f5cac0f5..e92a2313 100644 --- a/MCPForUnity/UnityMcpServer~/src/uv.lock +++ b/MCPForUnity/UnityMcpServer~/src/uv.lock @@ -1,14 +1,14 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.10" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] @@ -21,18 +21,27 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, ] [[package]] @@ -42,18 +51,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -63,18 +72,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "h11" version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, ] [[package]] @@ -85,9 +94,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196, upload-time = "2024-11-15T12:30:47.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551, upload-time = "2024-11-15T12:30:45.782Z" }, ] [[package]] @@ -100,27 +109,54 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] @@ -130,28 +166,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "mcp" -version = "1.4.1" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/79/5724a540df19e192e8606c543cdcf162de8eb435077520cca150f7365ec0/mcp-1.17.0.tar.gz", hash = "sha256:1b57fabf3203240ccc48e39859faf3ae1ccb0b571ff798bbedae800c73c6df90", size = 477951, upload-time = "2025-10-10T12:16:44.519Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, + { url = "https://files.pythonhosted.org/packages/1c/72/3751feae343a5ad07959df713907b5c3fbaed269d697a14b0c449080cf2e/mcp-1.17.0-py3-none-any.whl", hash = "sha256:0660ef275cada7a545af154db3082f176cf1d2681d5e35ae63e014faf0a35d40", size = 167737, upload-time = "2025-10-10T12:16:42.863Z" }, ] [package.optional-dependencies] @@ -162,115 +201,153 @@ cli = [ [[package]] name = "mcpforunityserver" -version = "3.1.0" +version = "6.0.0" source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "mcp", extra = ["cli"] }, + { name = "tomli" }, ] [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.4.1" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.15.0" }, + { name = "tomli", specifier = ">=2.3.0" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "pydantic" -version = "2.10.6" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2c/a5c4640dc7132540109f67fe83b566fbc7512ccf2a068cfa22a243df70c7/pydantic_core-2.41.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e63036298322e9aea1c8b7c0a6c1204d615dbf6ec0668ce5b83ff27f07404a61", size = 2113814, upload-time = "2025-10-06T21:09:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e7/a8694c3454a57842095d69c7a4ab3cf81c3c7b590f052738eabfdfc2e234/pydantic_core-2.41.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:241299ca91fc77ef64f11ed909d2d9220a01834e8e6f8de61275c4dd16b7c936", size = 1916660, upload-time = "2025-10-06T21:09:52.783Z" }, + { url = "https://files.pythonhosted.org/packages/9c/58/29f12e65b19c1877a0269eb4f23c5d2267eded6120a7d6762501ab843dc9/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab7e594a2a5c24ab8013a7dc8cfe5f2260e80e490685814122081705c2cf2b0", size = 1975071, upload-time = "2025-10-06T21:09:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/98/26/4e677f2b7ec3fbdd10be6b586a82a814c8ebe3e474024c8df2d4260e564e/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b054ef1a78519cb934b58e9c90c09e93b837c935dcd907b891f2b265b129eb6e", size = 2067271, upload-time = "2025-10-06T21:09:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/29/50/50614bd906089904d7ca1be3b9ecf08c00a327143d48f1decfdc21b3c302/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2ab7d10d0ab2ed6da54c757233eb0f48ebfb4f86e9b88ccecb3f92bbd61a538", size = 2253207, upload-time = "2025-10-06T21:09:56.709Z" }, + { url = "https://files.pythonhosted.org/packages/ea/58/b1e640b4ca559273cca7c28e0fe8891d5d8e9a600f5ab4882670ec107549/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2757606b7948bb853a27e4040820306eaa0ccb9e8f9f8a0fa40cb674e170f350", size = 2375052, upload-time = "2025-10-06T21:09:57.97Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/cd47df3bfb24350e03835f0950288d1054f1cc9a8023401dabe6d4ff2834/pydantic_core-2.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec0e75eb61f606bad0a32f2be87507087514e26e8c73db6cbdb8371ccd27917", size = 2076834, upload-time = "2025-10-06T21:09:59.58Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/71b2c77e5df527fbbc1a03e72c3fd96c44cd10d4241a81befef8c12b9fc4/pydantic_core-2.41.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0234236514f44a5bf552105cfe2543a12f48203397d9d0f866affa569345a5b5", size = 2195374, upload-time = "2025-10-06T21:10:01.18Z" }, + { url = "https://files.pythonhosted.org/packages/aa/08/4b8a50733005865efde284fec45da75fe16a258f706e16323c5ace4004eb/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1b974e41adfbb4ebb0f65fc4ca951347b17463d60893ba7d5f7b9bb087c83897", size = 2156060, upload-time = "2025-10-06T21:10:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/83/c3/1037cb603ef2130c210150a51b1710d86825b5c28df54a55750099f91196/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:248dafb3204136113c383e91a4d815269f51562b6659b756cf3df14eefc7d0bb", size = 2331640, upload-time = "2025-10-06T21:10:04.39Z" }, + { url = "https://files.pythonhosted.org/packages/56/4c/52d111869610e6b1a46e1f1035abcdc94d0655587e39104433a290e9f377/pydantic_core-2.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:678f9d76a91d6bcedd7568bbf6beb77ae8447f85d1aeebaab7e2f0829cfc3a13", size = 2329844, upload-time = "2025-10-06T21:10:05.68Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/4b435f0b52ab543967761aca66b84ad3f0026e491e57de47693d15d0a8db/pydantic_core-2.41.1-cp310-cp310-win32.whl", hash = "sha256:dff5bee1d21ee58277900692a641925d2dddfde65182c972569b1a276d2ac8fb", size = 1991289, upload-time = "2025-10-06T21:10:07.199Z" }, + { url = "https://files.pythonhosted.org/packages/88/52/31b4deafc1d3cb96d0e7c0af70f0dc05454982d135d07f5117e6336153e8/pydantic_core-2.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:5042da12e5d97d215f91567110fdfa2e2595a25f17c19b9ff024f31c34f9b53e", size = 2027747, upload-time = "2025-10-06T21:10:08.503Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1", size = 2111456, upload-time = "2025-10-06T21:10:09.824Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176", size = 1915012, upload-time = "2025-10-06T21:10:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0", size = 1973762, upload-time = "2025-10-06T21:10:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575", size = 2065386, upload-time = "2025-10-06T21:10:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20", size = 2252317, upload-time = "2025-10-06T21:10:15.719Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4", size = 2373405, upload-time = "2025-10-06T21:10:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d", size = 2073794, upload-time = "2025-10-06T21:10:18.383Z" }, + { url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1", size = 2194430, upload-time = "2025-10-06T21:10:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9", size = 2154611, upload-time = "2025-10-06T21:10:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1", size = 2329809, upload-time = "2025-10-06T21:10:22.678Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea", size = 2327907, upload-time = "2025-10-06T21:10:24.447Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl", hash = "sha256:2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f", size = 1989964, upload-time = "2025-10-06T21:10:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298", size = 2025158, upload-time = "2025-10-06T21:10:27.522Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl", hash = "sha256:7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5", size = 1972297, upload-time = "2025-10-06T21:10:28.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" }, + { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" }, + { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" }, + { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b", size = 2107836, upload-time = "2025-10-06T21:11:20.432Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a", size = 1904449, upload-time = "2025-10-06T21:11:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674", size = 1961750, upload-time = "2025-10-06T21:11:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4", size = 2063305, upload-time = "2025-10-06T21:11:26.556Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31", size = 2228959, upload-time = "2025-10-06T21:11:28.426Z" }, + { url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706", size = 2345421, upload-time = "2025-10-06T21:11:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b", size = 2065288, upload-time = "2025-10-06T21:11:32.019Z" }, + { url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be", size = 2189759, upload-time = "2025-10-06T21:11:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04", size = 2140747, upload-time = "2025-10-06T21:11:35.781Z" }, + { url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4", size = 2327416, upload-time = "2025-10-06T21:11:37.75Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8", size = 2318138, upload-time = "2025-10-06T21:11:39.463Z" }, + { url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159", size = 1998429, upload-time = "2025-10-06T21:11:41.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae", size = 2028870, upload-time = "2025-10-06T21:11:43.66Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9", size = 1974275, upload-time = "2025-10-06T21:11:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4", size = 1875124, upload-time = "2025-10-06T21:11:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e", size = 2043075, upload-time = "2025-10-06T21:11:49.542Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762", size = 1995341, upload-time = "2025-10-06T21:11:51.497Z" }, + { url = "https://files.pythonhosted.org/packages/16/89/d0afad37ba25f5801735af1472e650b86baad9fe807a42076508e4824a2a/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:68f2251559b8efa99041bb63571ec7cdd2d715ba74cc82b3bc9eff824ebc8bf0", size = 2124001, upload-time = "2025-10-07T10:49:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c4/08609134b34520568ddebb084d9ed0a2a3f5f52b45739e6e22cb3a7112eb/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:c7bc140c596097cb53b30546ca257dbe3f19282283190b1b5142928e5d5d3a20", size = 1941841, upload-time = "2025-10-07T10:49:56.248Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/94a4877094e5fe19a3f37e7e817772263e2c573c94f1e3fa2b1eee56ef3b/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2896510fce8f4725ec518f8b9d7f015a00db249d2fd40788f442af303480063d", size = 1961129, upload-time = "2025-10-07T10:49:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/a2/30/23a224d7e25260eb5f69783a63667453037e07eb91ff0e62dabaadd47128/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ced20e62cfa0f496ba68fa5d6c7ee71114ea67e2a5da3114d6450d7f4683572a", size = 2148770, upload-time = "2025-10-07T10:49:59.959Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" }, + { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/f403d7ca8352e3e4df352ccacd200f5f7f7fe81cef8e458515f015091625/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fabcbdb12de6eada8d6e9a759097adb3c15440fafc675b3e94ae5c9cb8d678a0", size = 2114268, upload-time = "2025-10-07T10:50:10.257Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b5/334473b6d2810df84db67f03d4f666acacfc538512c2d2a254074fee0889/pydantic_core-2.41.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e97ccfaf0aaf67d55de5085b0ed0d994f57747d9d03f2de5cc9847ca737b08", size = 1935786, upload-time = "2025-10-07T10:50:12.333Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/45513e4dc621f47397cfa5fef12ba8fa5e8b1c4c07f2ff2a5fef8ff81b25/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34df1fe8fea5d332484a763702e8b6a54048a9d4fe6ccf41e34a128238e01f52", size = 1971995, upload-time = "2025-10-07T10:50:14.071Z" }, + { url = "https://files.pythonhosted.org/packages/22/e3/f1797c168e5f52b973bed1c585e99827a22d5e579d1ed57d51bc15b14633/pydantic_core-2.41.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:421b5595f845842fc093f7250e24ee395f54ca62d494fdde96f43ecf9228ae01", size = 2191264, upload-time = "2025-10-07T10:50:15.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e1/24ef4c3b4ab91c21c3a09a966c7d2cffe101058a7bfe5cc8b2c7c7d574e2/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dce8b22663c134583aaad24827863306a933f576c79da450be3984924e2031d1", size = 2152430, upload-time = "2025-10-07T10:50:18.018Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/70c1e225d67f7ef3fdba02c506d9011efaf734020914920b2aa3d1a45e61/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:300a9c162fea9906cc5c103893ca2602afd84f0ec90d3be36f4cc360125d22e1", size = 2324691, upload-time = "2025-10-07T10:50:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/dd4d21037c8bef0d8cce90a86a3f2dcb011c30086db2a10113c3eea23eba/pydantic_core-2.41.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e019167628f6e6161ae7ab9fb70f6d076a0bf0d55aa9b20833f86a320c70dd65", size = 2324493, upload-time = "2025-10-07T10:50:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/7e/78/3093b334e9c9796c8236a4701cd2ddef1c56fb0928fe282a10c797644380/pydantic_core-2.41.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:13ab9cc2de6f9d4ab645a050ae5aee61a2424ac4d3a16ba23d4c2027705e0301", size = 2146156, upload-time = "2025-10-07T10:50:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1", size = 2112094, upload-time = "2025-10-07T10:50:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696", size = 1935300, upload-time = "2025-10-07T10:50:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222", size = 1970417, upload-time = "2025-10-07T10:50:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2", size = 2190745, upload-time = "2025-10-07T10:50:31.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506", size = 2150888, upload-time = "2025-10-07T10:50:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656", size = 2324489, upload-time = "2025-10-07T10:50:36.48Z" }, + { url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e", size = 2322763, upload-time = "2025-10-07T10:50:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb", size = 2144046, upload-time = "2025-10-07T10:50:40.686Z" }, ] [[package]] @@ -281,27 +358,72 @@ dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550, upload-time = "2025-02-27T10:10:32.338Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] name = "python-dotenv" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] @@ -313,27 +435,162 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" }, + { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" }, + { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" }, + { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" }, + { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" }, + { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" }, + { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] @@ -344,9 +601,9 @@ dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376, upload-time = "2024-12-25T09:09:30.616Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120, upload-time = "2024-12-25T09:09:26.761Z" }, ] [[package]] @@ -356,14 +613,63 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102, upload-time = "2025-03-08T10:55:34.504Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, + { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995, upload-time = "2025-03-08T10:55:32.662Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] name = "typer" -version = "0.15.2" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -371,18 +677,30 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] @@ -394,7 +712,7 @@ dependencies = [ { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, ] From e69387e8a6c14cef4d011a9372b4bd8f8b2aaeb0 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 13:41:01 -0400 Subject: [PATCH 02/42] feat: dynamically fetch package version from pyproject.toml for telemetry --- MCPForUnity/UnityMcpServer~/src/telemetry.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry.py b/MCPForUnity/UnityMcpServer~/src/telemetry.py index f7bfee78..ba7cae5a 100644 --- a/MCPForUnity/UnityMcpServer~/src/telemetry.py +++ b/MCPForUnity/UnityMcpServer~/src/telemetry.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from enum import Enum import importlib +import importlib.resources import json import logging import os @@ -24,6 +25,8 @@ from urllib.parse import urlparse import uuid +import tomllib + try: import httpx HAS_HTTPX = True @@ -34,6 +37,16 @@ logger = logging.getLogger("unity-mcp-telemetry") +def get_package_version() -> str: + pyproject_path = importlib.resources.files("pyproject.toml") + with pyproject_path.open("rb") as f: + data = tomllib.load(f) + return data["project"]["version"] + + +MCP_VERSION = get_package_version() + + class RecordType(str, Enum): """Types of telemetry records we collect""" VERSION = "version" @@ -328,7 +341,7 @@ def _send_telemetry(self, record: TelemetryRecord): "customer_uuid": record.customer_uuid, "session_id": record.session_id, "data": enriched_data, - "version": "3.0.2", # MCP for Unity version + "version": MCP_VERSION, "platform": _platform, "source": _source, } From 716e1f6ff38c5023b5b5065724caa4480ad7229e Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 20:39:35 -0400 Subject: [PATCH 03/42] Add pydantic --- MCPForUnity/UnityMcpServer~/src/pyproject.toml | 1 + MCPForUnity/UnityMcpServer~/src/uv.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/MCPForUnity/UnityMcpServer~/src/pyproject.toml b/MCPForUnity/UnityMcpServer~/src/pyproject.toml index 76137d9e..9a0d17e0 100644 --- a/MCPForUnity/UnityMcpServer~/src/pyproject.toml +++ b/MCPForUnity/UnityMcpServer~/src/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", "mcp[cli]>=1.15.0", + "pydantic>=2.12.0", "tomli>=2.3.0", ] diff --git a/MCPForUnity/UnityMcpServer~/src/uv.lock b/MCPForUnity/UnityMcpServer~/src/uv.lock index e92a2313..c7be8d1c 100644 --- a/MCPForUnity/UnityMcpServer~/src/uv.lock +++ b/MCPForUnity/UnityMcpServer~/src/uv.lock @@ -206,6 +206,7 @@ source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "mcp", extra = ["cli"] }, + { name = "pydantic" }, { name = "tomli" }, ] @@ -213,6 +214,7 @@ dependencies = [ requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, { name = "mcp", extras = ["cli"], specifier = ">=1.15.0" }, + { name = "pydantic", specifier = ">=2.12.0" }, { name = "tomli", specifier = ">=2.3.0" }, ] From 3270a5ccd4399ae959b459ae5a34c1e641ed8523 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 22:30:27 -0400 Subject: [PATCH 04/42] feat: add resource registry for MCP resource auto-discovery --- MCPForUnity/UnityMcpServer~/src/__init__.py | 3 -- .../UnityMcpServer~/src/registry/__init__.py | 12 ++++- .../src/registry/resource_registry.py | 53 +++++++++++++++++++ .../src/registry/tool_registry.py | 2 +- 4 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 MCPForUnity/UnityMcpServer~/src/registry/resource_registry.py diff --git a/MCPForUnity/UnityMcpServer~/src/__init__.py b/MCPForUnity/UnityMcpServer~/src/__init__.py index ad59ec7c..e69de29b 100644 --- a/MCPForUnity/UnityMcpServer~/src/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/__init__.py @@ -1,3 +0,0 @@ -""" -MCP for Unity Server package. -""" diff --git a/MCPForUnity/UnityMcpServer~/src/registry/__init__.py b/MCPForUnity/UnityMcpServer~/src/registry/__init__.py index 5beb708b..179da96d 100644 --- a/MCPForUnity/UnityMcpServer~/src/registry/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/registry/__init__.py @@ -4,11 +4,19 @@ from .tool_registry import ( mcp_for_unity_tool, get_registered_tools, - clear_registry + clear_tool_registry, +) +from .resource_registry import ( + mcp_for_unity_resource, + get_registered_resources, + clear_resource_registry, ) __all__ = [ 'mcp_for_unity_tool', 'get_registered_tools', - 'clear_registry' + 'clear_tool_registry', + 'mcp_for_unity_resource', + 'get_registered_resources', + 'clear_resource_registry' ] diff --git a/MCPForUnity/UnityMcpServer~/src/registry/resource_registry.py b/MCPForUnity/UnityMcpServer~/src/registry/resource_registry.py new file mode 100644 index 00000000..5c8e4260 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/registry/resource_registry.py @@ -0,0 +1,53 @@ +""" +Resource registry for auto-discovery of MCP resources. +""" +from typing import Callable, Any + +# Global registry to collect decorated resources +_resource_registry: list[dict[str, Any]] = [] + + +def mcp_for_unity_resource( + uri: str, + name: str | None = None, + description: str | None = None, + **kwargs +) -> Callable: + """ + Decorator for registering MCP resources in the server's resources directory. + + Resources are registered in the global resource registry. + + Args: + name: Resource name (defaults to function name) + description: Resource description + **kwargs: Additional arguments passed to @mcp.resource() + + Example: + @mcp_for_unity_resource("mcpforunity://resource", description="Gets something interesting") + async def my_custom_resource(ctx: Context, ...): + pass + """ + def decorator(func: Callable) -> Callable: + resource_name = name if name is not None else func.__name__ + _resource_registry.append({ + 'func': func, + 'uri': uri, + 'name': resource_name, + 'description': description, + 'kwargs': kwargs + }) + + return func + + return decorator + + +def get_registered_resources() -> list[dict[str, Any]]: + """Get all registered resources""" + return _resource_registry.copy() + + +def clear_resource_registry(): + """Clear the resource registry (useful for testing)""" + _resource_registry.clear() diff --git a/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py b/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py index bbe36439..08526a5a 100644 --- a/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py +++ b/MCPForUnity/UnityMcpServer~/src/registry/tool_registry.py @@ -46,6 +46,6 @@ def get_registered_tools() -> list[dict[str, Any]]: return _tool_registry.copy() -def clear_registry(): +def clear_tool_registry(): """Clear the tool registry (useful for testing)""" _tool_registry.clear() From 1e4070d4a5825e47e8d6b2d78cc8ae99f91190f2 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 22:33:21 -0400 Subject: [PATCH 05/42] feat: add telemetry decorator for tracking MCP resource usage --- .../src/telemetry_decorator.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py b/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py index 4683a50a..32f8a7cf 100644 --- a/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py +++ b/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py @@ -105,3 +105,60 @@ async def _async_wrapper(*args, **kwargs) -> Any: return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper return decorator + + +def telemetry_resource(resource_name: str): + """Decorator to add telemetry tracking to MCP resources""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def _sync_wrapper(*args, **kwargs) -> Any: + start_time = time.time() + success = False + error = None + try: + global _decorator_log_count + if _decorator_log_count < 10: + _log.info( + f"telemetry_decorator sync: resource={resource_name}") + _decorator_log_count += 1 + result = func(*args, **kwargs) + success = True + return result + except Exception as e: + error = str(e) + raise + finally: + duration_ms = (time.time() - start_time) * 1000 + try: + record_resource_usage(resource_name, success, + duration_ms, error) + except Exception: + _log.debug("record_resource_usage failed", exc_info=True) + + @functools.wraps(func) + async def _async_wrapper(*args, **kwargs) -> Any: + start_time = time.time() + success = False + error = None + try: + global _decorator_log_count + if _decorator_log_count < 10: + _log.info( + f"telemetry_decorator async: resource={resource_name}") + _decorator_log_count += 1 + result = await func(*args, **kwargs) + success = True + return result + except Exception as e: + error = str(e) + raise + finally: + duration_ms = (time.time() - start_time) * 1000 + try: + record_resource_usage(resource_name, success, + duration_ms, error) + except Exception: + _log.debug("record_resource_usage failed", exc_info=True) + + return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper + return decorator From 560b9d0d1390a0887f5d046b156d7e10e1502ce2 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 22:33:47 -0400 Subject: [PATCH 06/42] feat: add auto-discovery and registration system for MCP resources --- .../UnityMcpServer~/src/resources/__init__.py | 61 +++++++++++++++++++ .../UnityMcpServer~/src/tools/__init__.py | 4 +- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 MCPForUnity/UnityMcpServer~/src/resources/__init__.py diff --git a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py new file mode 100644 index 00000000..37fa720a --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py @@ -0,0 +1,61 @@ +""" +MCP Resources package - Auto-discovers and registers all resources in this directory. +""" +import importlib +import logging +from pathlib import Path +import pkgutil + +from mcp.server.fastmcp import FastMCP +from telemetry_decorator import telemetry_resource + +from registry import get_registered_resources + +logger = logging.getLogger("mcp-for-unity-server") + +# Export decorator for easy imports within tools +__all__ = ['register_all_resources'] + + +def register_all_resources(mcp: FastMCP): + """ + Auto-discover and register all resources in the resources/ directory. + + Any .py file in this directory with @mcp_for_unity_resource decorated + functions will be automatically registered. + """ + logger.info("Auto-discovering MCP for Unity Server resources...") + # Dynamic import of all modules in this directory + resources_dir = Path(__file__).parent + + for _, module_name, _ in pkgutil.iter_modules([str(resources_dir)]): + # Skip private modules and __init__ + if module_name.startswith('_'): + continue + + try: + importlib.import_module(f'.{module_name}', __package__) + except Exception as e: + logger.warning( + f"Failed to import resource module {module_name}: {e}") + + resources = get_registered_resources() + + if not resources: + logger.warning("No MCP resources registered!") + return + + for resource_info in resources: + func = resource_info['func'] + resource_name = resource_info['name'] + description = resource_info['description'] + kwargs = resource_info['kwargs'] + + # Apply the @mcp.resource decorator and telemetry + wrapped = mcp.resource( + name=resource_name, description=description, **kwargs)(func) + wrapped = telemetry_resource(resource_name)(wrapped) + resource_info['func'] = wrapped + logger.info(f"Registered resource: {resource_name} - {description}") + + logger.info(f"Registered {len(resources)} MCP resources") diff --git a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py index 6ede53d3..249f2bed 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py @@ -9,12 +9,12 @@ from mcp.server.fastmcp import FastMCP from telemetry_decorator import telemetry_tool -from registry import get_registered_tools, mcp_for_unity_tool +from registry import get_registered_tools logger = logging.getLogger("mcp-for-unity-server") # Export decorator for easy imports within tools -__all__ = ['register_all_tools', 'mcp_for_unity_tool'] +__all__ = ['register_all_tools'] def register_all_tools(mcp: FastMCP): From b81919d55ce90a6b2c35af0673659f21e47907bc Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 22:34:02 -0400 Subject: [PATCH 07/42] feat: add resource registration to MCP server initialization --- MCPForUnity/UnityMcpServer~/src/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/server.py b/MCPForUnity/UnityMcpServer~/src/server.py index af6fe036..acab160b 100644 --- a/MCPForUnity/UnityMcpServer~/src/server.py +++ b/MCPForUnity/UnityMcpServer~/src/server.py @@ -7,6 +7,7 @@ from typing import AsyncIterator, Dict, Any from config import config from tools import register_all_tools +from resources import register_all_resources from unity_connection import get_unity_connection, UnityConnection import time @@ -162,7 +163,8 @@ def _emit_startup(): # Register all tools register_all_tools(mcp) -# Asset Creation Strategy +# Register all resources +register_all_resources(mcp) @mcp.prompt() From e00899bf95c4269872ed18977572ed15177d9ef5 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 22:34:12 -0400 Subject: [PATCH 08/42] feat: add MCPResponse model class for standardized API responses --- MCPForUnity/UnityMcpServer~/src/models.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 MCPForUnity/UnityMcpServer~/src/models.py diff --git a/MCPForUnity/UnityMcpServer~/src/models.py b/MCPForUnity/UnityMcpServer~/src/models.py new file mode 100644 index 00000000..6461a30c --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/models.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class MCPResponse(BaseModel): + success: bool + message: str + data: Any | None = None From b1df08fca13287eae456ffe25a34c6501b64cf5f Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 22:48:29 -0400 Subject: [PATCH 09/42] refactor: replace Debug.Log calls with McpLog wrapper for consistent logging --- MCPForUnity/Editor/MCPForUnityBridge.cs | 38 ++++++++++++------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs b/MCPForUnity/Editor/MCPForUnityBridge.cs index dcc469b0..0a46243e 100644 --- a/MCPForUnity/Editor/MCPForUnityBridge.cs +++ b/MCPForUnity/Editor/MCPForUnityBridge.cs @@ -96,7 +96,7 @@ public static void StartAutoConnect() } catch (Exception ex) { - Debug.LogError($"Auto-connect failed: {ex.Message}"); + McpLog.Error($"Auto-connect failed: {ex.Message}"); // Record telemetry for connection failure TelemetryHelper.RecordBridgeConnection(false, ex.Message); @@ -297,7 +297,7 @@ public static void Start() { if (IsDebugEnabled()) { - Debug.Log($"MCP-FOR-UNITY: MCPForUnityBridge already running on port {currentUnityPort}"); + McpLog.Info($"MCPForUnityBridge already running on port {currentUnityPort}"); } return; } @@ -383,7 +383,7 @@ public static void Start() isAutoConnectMode = false; string platform = Application.platform.ToString(); string serverVer = ReadInstalledServerVersionSafe(); - Debug.Log($"MCP-FOR-UNITY: MCPForUnityBridge started on port {currentUnityPort}. (OS={platform}, server={serverVer})"); + McpLog.Info($"MCPForUnityBridge started on port {currentUnityPort}. (OS={platform}, server={serverVer})"); // Start background listener with cooperative cancellation cts = new CancellationTokenSource(); listenerTask = Task.Run(() => ListenerLoopAsync(cts.Token)); @@ -403,7 +403,7 @@ public static void Start() } catch (SocketException ex) { - Debug.LogError($"Failed to start TCP listener: {ex.Message}"); + McpLog.Error($"Failed to start TCP listener: {ex.Message}"); } } } @@ -437,7 +437,7 @@ public static void Stop() } catch (Exception ex) { - Debug.LogError($"Error stopping MCPForUnityBridge: {ex.Message}"); + McpLog.Error($"Error stopping MCPForUnityBridge: {ex.Message}"); } } @@ -465,7 +465,7 @@ public static void Stop() try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } try { EditorApplication.quitting -= Stop; } catch { } - if (IsDebugEnabled()) Debug.Log("MCP-FOR-UNITY: MCPForUnityBridge stopped."); + if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped."); } private static async Task ListenerLoopAsync(CancellationToken token) @@ -504,7 +504,7 @@ private static async Task ListenerLoopAsync(CancellationToken token) { if (isRunning && !token.IsCancellationRequested) { - if (IsDebugEnabled()) Debug.LogError($"Listener error: {ex.Message}"); + if (IsDebugEnabled()) McpLog.Error($"Listener error: {ex.Message}"); } } } @@ -524,7 +524,7 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken if (IsDebugEnabled()) { var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown"; - Debug.Log($"UNITY-MCP: Client connected {ep}"); + McpLog.Info($"Client connected {ep}"); } } catch { } @@ -544,11 +544,11 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken #else await stream.WriteAsync(handshakeBytes, 0, handshakeBytes.Length, cts.Token).ConfigureAwait(false); #endif - if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false); + if (IsDebugEnabled()) McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false); } catch (Exception ex) { - if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Warn($"Handshake failed: {ex.Message}"); + if (IsDebugEnabled()) McpLog.Warn($"Handshake failed: {ex.Message}"); return; // abort this client } @@ -564,7 +564,7 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken if (IsDebugEnabled()) { var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText; - MCPForUnity.Editor.Helpers.McpLog.Info($"recv framed: {preview}", always: false); + McpLog.Info($"recv framed: {preview}", always: false); } } catch { } @@ -623,7 +623,7 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken if (IsDebugEnabled()) { - try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { } + try { McpLog.Info("[MCP] sending framed response", always: false); } catch { } } // Crash-proof and self-reporting writer logs (direct write to this client's stream) long seq = System.Threading.Interlocked.Increment(ref _ioSeq); @@ -662,11 +662,11 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken || ex is System.IO.IOException; if (isBenign) { - if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info($"Client handler: {msg}", always: false); + if (IsDebugEnabled()) McpLog.Info($"Client handler: {msg}", always: false); } else { - MCPForUnity.Editor.Helpers.McpLog.Error($"Client handler error: {msg}"); + McpLog.Error($"Client handler error: {msg}"); } break; } @@ -900,7 +900,7 @@ private static void ProcessCommands() } catch (Exception ex) { - Debug.LogError($"Error processing command: {ex.Message}\n{ex.StackTrace}"); + McpLog.Error($"Error processing command: {ex.Message}\n{ex.StackTrace}"); var response = new { @@ -1051,9 +1051,7 @@ private static string ExecuteCommand(Command command) catch (Exception ex) { // Log the detailed error in Unity for debugging - Debug.LogError( - $"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}" - ); + McpLog.Error($"Error executing command '{command?.type ?? "Unknown"}': {ex.Message}\n{ex.StackTrace}"); // Standard error response format var response = new @@ -1074,11 +1072,11 @@ private static object HandleManageScene(JObject paramsObject) { try { - if (IsDebugEnabled()) Debug.Log("[MCP] manage_scene: dispatching to main thread"); + if (IsDebugEnabled()) McpLog.Info("[MCP] manage_scene: dispatching to main thread"); var sw = System.Diagnostics.Stopwatch.StartNew(); var r = InvokeOnMainThreadWithTimeout(() => ManageScene.HandleCommand(paramsObject), FrameIOTimeoutMs); sw.Stop(); - if (IsDebugEnabled()) Debug.Log($"[MCP] manage_scene: completed in {sw.ElapsedMilliseconds} ms"); + if (IsDebugEnabled()) McpLog.Info($"[MCP] manage_scene: completed in {sw.ElapsedMilliseconds} ms"); return r ?? Response.Error("manage_scene returned null (timeout or error)"); } catch (Exception ex) From eb33612b0d8251e253113cc02844bbc834987a24 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 11 Oct 2025 23:54:30 -0400 Subject: [PATCH 10/42] feat: add test discovery endpoints for Unity Test Framework integration We haven't connected them as yet, still thinking about how to do this neatly --- MCPForUnity/Editor/Resources.meta | 8 + .../Resources/McpForUnityResourceAttribute.cs | 37 +++ .../McpForUnityResourceAttribute.cs.meta | 11 + MCPForUnity/Editor/Resources/Tests.meta | 8 + .../Editor/Resources/Tests/GetTests.cs | 216 ++++++++++++++++++ .../Editor/Resources/Tests/GetTests.cs.meta | 11 + .../UnityMcpServer~/src/resources/tests.py | 35 +++ 7 files changed, 326 insertions(+) create mode 100644 MCPForUnity/Editor/Resources.meta create mode 100644 MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs create mode 100644 MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta create mode 100644 MCPForUnity/Editor/Resources/Tests.meta create mode 100644 MCPForUnity/Editor/Resources/Tests/GetTests.cs create mode 100644 MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta create mode 100644 MCPForUnity/UnityMcpServer~/src/resources/tests.py diff --git a/MCPForUnity/Editor/Resources.meta b/MCPForUnity/Editor/Resources.meta new file mode 100644 index 00000000..8d921dfd --- /dev/null +++ b/MCPForUnity/Editor/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a6f5bafffbb0f48c2a33ad9470bb1e2d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs b/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs new file mode 100644 index 00000000..9b895e23 --- /dev/null +++ b/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs @@ -0,0 +1,37 @@ +using System; + +namespace MCPForUnity.Editor.Resources +{ + /// + /// Marks a class as an MCP resource handler for auto-discovery. + /// The class must have a public static HandleCommand(JObject) method. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class McpForUnityResourceAttribute : Attribute + { + /// + /// The resource name used to route requests to this resource. + /// If not specified, defaults to the PascalCase class name converted to snake_case. + /// + public string ResourceName { get; } + + /// + /// Create an MCP resource attribute with auto-generated resource name. + /// The resource name will be derived from the class name (PascalCase → snake_case). + /// Example: ManageAsset → manage_asset + /// + public McpForUnityResourceAttribute() + { + ResourceName = null; // Will be auto-generated + } + + /// + /// Create an MCP resource attribute with explicit resource name. + /// + /// The resource name (e.g., "manage_asset") + public McpForUnityResourceAttribute(string resourceName) + { + ResourceName = resourceName; + } + } +} diff --git a/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta b/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta new file mode 100644 index 00000000..e887db08 --- /dev/null +++ b/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c2d60f570f3d4bd2a6a2c1293094be3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Tests.meta b/MCPForUnity/Editor/Resources/Tests.meta new file mode 100644 index 00000000..0aa0bf04 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 412726d2e774048939b0d2bd4f11a503 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs new file mode 100644 index 00000000..5ff7a108 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using UnityEditor.TestTools.TestRunner.Api; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Resources.Tests +{ + /// + /// Provides access to Unity tests from the Test Framework. + /// This is a read-only resource that can be queried by MCP clients. + /// + [McpForUnityResource("get_tests")] + public static class GetTests + { + public static object HandleCommand(JObject @params) + { + try + { + string modeStr = @params?["mode"]?.ToString(); + TestMode? parsedMode = null; + + if (!string.IsNullOrWhiteSpace(modeStr)) + { + if (!ModeParser.TryParse(modeStr, out parsedMode, out var error)) + { + return Response.Error(error); + } + } + + McpLog.Info( + parsedMode.HasValue + ? $"[GetTests] Retrieving tests for mode: {parsedMode.Value}" + : "[GetTests] Retrieving tests for all modes", + always: false + ); + + var tests = TestCollector.GetTests(parsedMode); + string message = parsedMode.HasValue + ? $"Retrieved {tests.Count} {parsedMode.Value} tests" + : $"Retrieved {tests.Count} tests"; + + return Response.Success(message, tests); + } + catch (Exception e) + { + McpLog.Error($"[GetTests] Error retrieving tests: {e.Message}"); + return Response.Error($"Error retrieving tests: {e.Message}"); + } + } + } + + /// + /// Provides access to Unity tests for a specific mode (EditMode or PlayMode). + /// This is a read-only resource that can be queried by MCP clients. + /// + [McpForUnityResource("get_tests_for_mode")] + public static class GetTestsForMode + { + public static object HandleCommand(JObject @params) + { + try + { + string modeStr = @params["mode"]?.ToString(); + if (string.IsNullOrEmpty(modeStr)) + { + return Response.Error("'mode' parameter is required"); + } + + if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) + { + return Response.Error(parseError); + } + + McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}", always: false); + + var tests = TestCollector.GetTests(parsedMode); + string message = $"Retrieved {tests.Count} {parsedMode.Value} tests"; + return Response.Success(message, tests); + } + catch (Exception e) + { + McpLog.Error($"[GetTestsForMode] Error retrieving tests: {e.Message}"); + return Response.Error($"Error retrieving tests: {e.Message}"); + } + } + } + + internal static class TestCollector + { + private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode }; + + internal static List> GetTests(TestMode? filterMode) + { + var modesToQuery = filterMode.HasValue ? new[] { filterMode.Value } : AllModes; + var tests = new List>(); + var seen = new HashSet(StringComparer.Ordinal); + + var api = ScriptableObject.CreateInstance(); + + try + { + foreach (var mode in modesToQuery) + { + var filter = new Filter { testMode = mode }; + api.RetrieveTestList(filter, root => + { + if (root == null) + { + return; + } + + CollectFromNode(root, mode, tests, seen, new List()); + }); + } + } + finally + { + if (api != null) + { + ScriptableObject.DestroyImmediate(api); + } + } + + return tests; + } + + private static void CollectFromNode( + ITestAdaptor node, + TestMode mode, + List> output, + HashSet seen, + List path + ) + { + if (node == null) + { + return; + } + + bool hasName = !string.IsNullOrEmpty(node.Name); + if (hasName) + { + path.Add(node.Name); + } + + bool hasChildren = node.HasChildren && node.Children != null; + + if (!hasChildren) + { + string fullName = string.IsNullOrEmpty(node.FullName) ? node.Name ?? string.Empty : node.FullName; + string key = $"{mode}:{fullName}"; + + if (!string.IsNullOrEmpty(fullName) && seen.Add(key)) + { + string computedPath = path.Count > 0 ? string.Join("/", path) : fullName; + output.Add(new Dictionary + { + ["name"] = node.Name ?? fullName, + ["full_name"] = fullName, + ["path"] = computedPath, + ["mode"] = mode.ToString(), + }); + } + } + else + { + foreach (var child in node.Children) + { + CollectFromNode(child, mode, output, seen, path); + } + } + + if (hasName) + { + path.RemoveAt(path.Count - 1); + } + } + } + + internal static class ModeParser + { + internal static bool TryParse(string modeStr, out TestMode? mode, out string error) + { + error = null; + mode = null; + + if (string.IsNullOrWhiteSpace(modeStr)) + { + error = "'mode' parameter cannot be empty"; + return false; + } + + if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase) || + modeStr.Equals("editmode", StringComparison.OrdinalIgnoreCase) || + modeStr.Equals("EditMode", StringComparison.Ordinal)) + { + mode = TestMode.EditMode; + return true; + } + + if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase) || + modeStr.Equals("playmode", StringComparison.OrdinalIgnoreCase) || + modeStr.Equals("PlayMode", StringComparison.Ordinal)) + { + mode = TestMode.PlayMode; + return true; + } + + error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'"; + return false; + } + } +} diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta b/MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta new file mode 100644 index 00000000..aa419737 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84183aaed077e4f25968269c952db2d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tests.py b/MCPForUnity/UnityMcpServer~/src/resources/tests.py new file mode 100644 index 00000000..637e511c --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/tests.py @@ -0,0 +1,35 @@ +from typing import Annotated, Literal +from pydantic import BaseModel, Field +from mcp.server.fastmcp import Context + +from ..models import MCPResponse +from registry import mcp_for_unity_resource +from unity_connection import async_send_command_with_retry + + +class TestItem(BaseModel): + name: Annotated[str, Field(description="The name of the test.")] + full_name: Annotated[str, Field(description="The full name of the test.")] + path: Annotated[str, Field(description="The path of the test.")] + mode: Annotated[str, Field( + description="The mode the test is for (EditMode or PlayMode).")] + + +class GetTestsResponse(MCPResponse): + data: list[TestItem] + + +@mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.") +async def get_tests(ctx: Context) -> GetTestsResponse: + ctx.info("Getting all tests") + """Provides a list of all tests.""" + response = await async_send_command_with_retry("get_tests") + return GetTestsResponse(**response) + + +@mcp_for_unity_resource(uri="mcpforunity://tests/{mode}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.") +async def get_tests_for_mode(ctx: Context, mode: Annotated[Literal["edit", "play"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse: + ctx.info(f"Getting tests for mode: {mode}") + """Provides a list of tests for a specific mode.""" + response = await async_send_command_with_retry("get_tests_for_mode", {"mode": mode}) + return GetTestsResponse(**response) From 2d2e62a8cbb9a5dd01a48d21a7859d84afae75b3 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 00:30:51 -0400 Subject: [PATCH 11/42] Fix server setup --- MCPForUnity/UnityMcpServer~/src/models.py | 1 + MCPForUnity/UnityMcpServer~/src/resources/__init__.py | 5 +++-- MCPForUnity/UnityMcpServer~/src/resources/tests.py | 2 +- MCPForUnity/UnityMcpServer~/src/telemetry.py | 11 +++++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/models.py b/MCPForUnity/UnityMcpServer~/src/models.py index 6461a30c..6aa8a5f9 100644 --- a/MCPForUnity/UnityMcpServer~/src/models.py +++ b/MCPForUnity/UnityMcpServer~/src/models.py @@ -1,3 +1,4 @@ +from typing import Any from pydantic import BaseModel diff --git a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py index 37fa720a..6ecd6f12 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py @@ -47,13 +47,14 @@ def register_all_resources(mcp: FastMCP): for resource_info in resources: func = resource_info['func'] + uri = resource_info['uri'] resource_name = resource_info['name'] description = resource_info['description'] kwargs = resource_info['kwargs'] # Apply the @mcp.resource decorator and telemetry - wrapped = mcp.resource( - name=resource_name, description=description, **kwargs)(func) + wrapped = mcp.resource(uri=uri, name=resource_name, + description=description, **kwargs)(func) wrapped = telemetry_resource(resource_name)(wrapped) resource_info['func'] = wrapped logger.info(f"Registered resource: {resource_name} - {description}") diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tests.py b/MCPForUnity/UnityMcpServer~/src/resources/tests.py index 637e511c..83f58717 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/tests.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/tests.py @@ -2,7 +2,7 @@ from pydantic import BaseModel, Field from mcp.server.fastmcp import Context -from ..models import MCPResponse +from models import MCPResponse from registry import mcp_for_unity_resource from unity_connection import async_send_command_with_retry diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry.py b/MCPForUnity/UnityMcpServer~/src/telemetry.py index ba7cae5a..aa8b8ab4 100644 --- a/MCPForUnity/UnityMcpServer~/src/telemetry.py +++ b/MCPForUnity/UnityMcpServer~/src/telemetry.py @@ -11,7 +11,6 @@ from dataclasses import dataclass from enum import Enum import importlib -import importlib.resources import json import logging import os @@ -38,8 +37,10 @@ def get_package_version() -> str: - pyproject_path = importlib.resources.files("pyproject.toml") - with pyproject_path.open("rb") as f: + """ + Open pyproject.toml and parse version + """ + with open("pyproject.toml", "rb") as f: data = tomllib.load(f) return data["project"]["version"] @@ -85,7 +86,9 @@ class TelemetryConfig: """Telemetry configuration""" def __init__(self): - # Prefer config file, then allow env overrides + """ + Prefer config file, then allow env overrides + """ server_config = None for modname in ( "MCPForUnity.UnityMcpServer~.src.config", From 8c1300e3985c8e523c2f521c5ca3f7f4d277a98c Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 00:43:34 -0400 Subject: [PATCH 12/42] refactor: reduce log verbosity by changing individual resource/tool registration logs to debug level --- MCPForUnity/UnityMcpServer~/src/resources/__init__.py | 4 ++-- MCPForUnity/UnityMcpServer~/src/tools/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py index 6ecd6f12..964cfa9c 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py @@ -53,10 +53,10 @@ def register_all_resources(mcp: FastMCP): kwargs = resource_info['kwargs'] # Apply the @mcp.resource decorator and telemetry - wrapped = mcp.resource(uri=uri, name=resource_name, + wrapped = mcp.resource(uri, name=resource_name, description=description, **kwargs)(func) wrapped = telemetry_resource(resource_name)(wrapped) resource_info['func'] = wrapped - logger.info(f"Registered resource: {resource_name} - {description}") + logger.debug(f"Registered resource: {resource_name} - {description}") logger.info(f"Registered {len(resources)} MCP resources") diff --git a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py index 249f2bed..ccaf7d95 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py @@ -55,6 +55,6 @@ def register_all_tools(mcp: FastMCP): name=tool_name, description=description, **kwargs)(func) wrapped = telemetry_tool(tool_name)(wrapped) tool_info['func'] = wrapped - logger.info(f"Registered tool: {tool_name} - {description}") + logger.debug(f"Registered tool: {tool_name} - {description}") logger.info(f"Registered {len(tools)} MCP tools") From bbde287fa5737210ae63d314c12ed0e0d819e558 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 01:09:51 -0400 Subject: [PATCH 13/42] chore: bump mcp[cli] dependency from 1.15.0 to 1.17.0 --- MCPForUnity/UnityMcpServer~/src/pyproject.toml | 2 +- MCPForUnity/UnityMcpServer~/src/uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/pyproject.toml b/MCPForUnity/UnityMcpServer~/src/pyproject.toml index 9a0d17e0..1f5fd4df 100644 --- a/MCPForUnity/UnityMcpServer~/src/pyproject.toml +++ b/MCPForUnity/UnityMcpServer~/src/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", - "mcp[cli]>=1.15.0", + "mcp[cli]>=1.17.0", "pydantic>=2.12.0", "tomli>=2.3.0", ] diff --git a/MCPForUnity/UnityMcpServer~/src/uv.lock b/MCPForUnity/UnityMcpServer~/src/uv.lock index c7be8d1c..b6d29e27 100644 --- a/MCPForUnity/UnityMcpServer~/src/uv.lock +++ b/MCPForUnity/UnityMcpServer~/src/uv.lock @@ -213,7 +213,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.15.0" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.17.0" }, { name = "pydantic", specifier = ">=2.12.0" }, { name = "tomli", specifier = ">=2.3.0" }, ] From 1fe28e45f9eb336ccac90fd3dab3b1ca0f34ec53 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 01:10:28 -0400 Subject: [PATCH 14/42] refactor: remove Context parameter and add uri keyword argument in resource decorator The Context parameter doesn't work on our version of FastMCP --- MCPForUnity/UnityMcpServer~/src/resources/__init__.py | 2 +- MCPForUnity/UnityMcpServer~/src/resources/tests.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py index 964cfa9c..fe5bd8d6 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py @@ -53,7 +53,7 @@ def register_all_resources(mcp: FastMCP): kwargs = resource_info['kwargs'] # Apply the @mcp.resource decorator and telemetry - wrapped = mcp.resource(uri, name=resource_name, + wrapped = mcp.resource(uri=uri, name=resource_name, description=description, **kwargs)(func) wrapped = telemetry_resource(resource_name)(wrapped) resource_info['func'] = wrapped diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tests.py b/MCPForUnity/UnityMcpServer~/src/resources/tests.py index 83f58717..fc182e2e 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/tests.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/tests.py @@ -1,6 +1,5 @@ from typing import Annotated, Literal from pydantic import BaseModel, Field -from mcp.server.fastmcp import Context from models import MCPResponse from registry import mcp_for_unity_resource @@ -20,16 +19,14 @@ class GetTestsResponse(MCPResponse): @mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.") -async def get_tests(ctx: Context) -> GetTestsResponse: - ctx.info("Getting all tests") +async def get_tests() -> GetTestsResponse: """Provides a list of all tests.""" - response = await async_send_command_with_retry("get_tests") + response = await async_send_command_with_retry("get_tests", {}) return GetTestsResponse(**response) @mcp_for_unity_resource(uri="mcpforunity://tests/{mode}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.") -async def get_tests_for_mode(ctx: Context, mode: Annotated[Literal["edit", "play"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse: - ctx.info(f"Getting tests for mode: {mode}") +async def get_tests_for_mode(mode: Annotated[Literal["edit", "play"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse: """Provides a list of tests for a specific mode.""" response = await async_send_command_with_retry("get_tests_for_mode", {"mode": mode}) return GetTestsResponse(**response) From 6f0e8e18616c21baf0cebbb9a866990d275b2ec6 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 01:37:09 -0400 Subject: [PATCH 15/42] chore: upgrade Python base image to 3.13 and simplify Dockerfile setup --- MCPForUnity/UnityMcpServer~/src/Dockerfile | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/Dockerfile b/MCPForUnity/UnityMcpServer~/src/Dockerfile index 5fcbc4eb..7c2ec81d 100644 --- a/MCPForUnity/UnityMcpServer~/src/Dockerfile +++ b/MCPForUnity/UnityMcpServer~/src/Dockerfile @@ -1,27 +1,15 @@ -FROM python:3.12-slim +FROM python:3.13-slim -# Install required system dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ git \ && rm -rf /var/lib/apt/lists/* -# Set working directory WORKDIR /app -# Install uv package manager RUN pip install uv -# Copy required files -COPY config.py /app/ -COPY server.py /app/ -COPY unity_connection.py /app/ -COPY pyproject.toml /app/ -COPY __init__.py /app/ -COPY tools/ /app/tools/ +COPY . /app -# Install dependencies using uv -RUN uv pip install --system -e . +RUN uv sync - -# Command to run the server CMD ["uv", "run", "server.py"] From 57c699b6456d564ec87c86aa0fd3f851b1e9afd4 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 14:11:39 -0400 Subject: [PATCH 16/42] fix: apply telemetry decorator before mcp.tool to ensure proper wrapping order --- MCPForUnity/UnityMcpServer~/src/tools/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py index ccaf7d95..77ec7f61 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py @@ -51,9 +51,9 @@ def register_all_tools(mcp: FastMCP): kwargs = tool_info['kwargs'] # Apply the @mcp.tool decorator and telemetry + wrapped = telemetry_tool(tool_name)(func) wrapped = mcp.tool( - name=tool_name, description=description, **kwargs)(func) - wrapped = telemetry_tool(tool_name)(wrapped) + name=tool_name, description=description, **kwargs)(wrapped) tool_info['func'] = wrapped logger.debug(f"Registered tool: {tool_name} - {description}") From 2c30710acfd297c3783a425bde16de9119475fac Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 14:11:52 -0400 Subject: [PATCH 17/42] fix: swap order of telemetry and resource decorators to properly wrap handlers --- MCPForUnity/UnityMcpServer~/src/resources/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py index fe5bd8d6..23c5604a 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/__init__.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/__init__.py @@ -53,9 +53,9 @@ def register_all_resources(mcp: FastMCP): kwargs = resource_info['kwargs'] # Apply the @mcp.resource decorator and telemetry + wrapped = telemetry_resource(resource_name)(func) wrapped = mcp.resource(uri=uri, name=resource_name, - description=description, **kwargs)(func) - wrapped = telemetry_resource(resource_name)(wrapped) + description=description, **kwargs)(wrapped) resource_info['func'] = wrapped logger.debug(f"Registered resource: {resource_name} - {description}") From d577de85556956a3f1cb08fd864bc01c9717b855 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 14:27:24 -0400 Subject: [PATCH 18/42] fix: update log prefixes for consistency in logging methods --- MCPForUnity/Editor/Helpers/McpLog.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MCPForUnity/Editor/Helpers/McpLog.cs b/MCPForUnity/Editor/Helpers/McpLog.cs index 85abdb79..8d31c556 100644 --- a/MCPForUnity/Editor/Helpers/McpLog.cs +++ b/MCPForUnity/Editor/Helpers/McpLog.cs @@ -5,7 +5,9 @@ namespace MCPForUnity.Editor.Helpers { internal static class McpLog { - private const string Prefix = "MCP-FOR-UNITY:"; + private const string LogPrefix = "MCP-FOR-UNITY:"; + private const string WarnPrefix = "MCP-FOR-UNITY:"; + private const string ErrorPrefix = "MCP-FOR-UNITY:"; private static bool IsDebugEnabled() { @@ -15,17 +17,17 @@ private static bool IsDebugEnabled() public static void Info(string message, bool always = true) { if (!always && !IsDebugEnabled()) return; - Debug.Log($"{Prefix} {message}"); + Debug.Log($"{LogPrefix} {message}"); } public static void Warn(string message) { - Debug.LogWarning($"{Prefix} {message}"); + Debug.LogWarning($"{WarnPrefix} {message}"); } public static void Error(string message) { - Debug.LogError($"{Prefix} {message}"); + Debug.LogError($"{ErrorPrefix} {message}"); } } } From 48eb897efe86a97fc4f039651e638eba57088a4e Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 14:38:42 -0400 Subject: [PATCH 19/42] Fix compile errors --- MCPForUnity/Editor/Resources/Tests/GetTests.cs | 4 ++-- MCPForUnity/Editor/Tools/ManageEditor.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index 5ff7a108..c62e8ab5 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -104,8 +104,8 @@ internal static List> GetTests(TestMode? filterMode) { foreach (var mode in modesToQuery) { - var filter = new Filter { testMode = mode }; - api.RetrieveTestList(filter, root => + var filter = new Filter(); + api.RetrieveTestList(mode, root => { if (root == null) { diff --git a/MCPForUnity/Editor/Tools/ManageEditor.cs b/MCPForUnity/Editor/Tools/ManageEditor.cs index f8255224..97a20a4e 100644 --- a/MCPForUnity/Editor/Tools/ManageEditor.cs +++ b/MCPForUnity/Editor/Tools/ManageEditor.cs @@ -206,7 +206,7 @@ private static object GetEditorWindows() // Find currently open instances // Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows - EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll(); + EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll(); foreach (EditorWindow window in allWindows) { From 5cb35112d27a4858397f9c87936e38b647eb9c40 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 14:45:26 -0400 Subject: [PATCH 20/42] feat: extend command registry to support both tools and resources --- MCPForUnity/Editor/Tools/CommandRegistry.cs | 68 +++++++++++++++------ 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs index 79003d55..a93dbead 100644 --- a/MCPForUnity/Editor/Tools/CommandRegistry.cs +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs @@ -4,12 +4,14 @@ using System.Reflection; using System.Text.RegularExpressions; using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Resources; using Newtonsoft.Json.Linq; namespace MCPForUnity.Editor.Tools { /// /// Registry for all MCP command handlers via reflection. + /// Handles both MCP tools and resources. /// public static class CommandRegistry { @@ -17,13 +19,14 @@ public static class CommandRegistry private static bool _initialized = false; /// - /// Initialize and auto-discover all tools marked with [McpForUnityTool] + /// Initialize and auto-discover all tools and resources marked with + /// [McpForUnityTool] or [McpForUnityResource] /// public static void Initialize() { if (_initialized) return; - AutoDiscoverTools(); + AutoDiscoverCommands(); _initialized = true; } @@ -41,40 +44,69 @@ private static string ToSnakeCase(string name) } /// - /// Auto-discover all types with [McpForUnityTool] attribute + /// Auto-discover all types with [McpForUnityTool] or [McpForUnityResource] attributes /// - private static void AutoDiscoverTools() + private static void AutoDiscoverCommands() { try { - var toolTypes = AppDomain.CurrentDomain.GetAssemblies() + var allTypes = AppDomain.CurrentDomain.GetAssemblies() .Where(a => !a.IsDynamic) .SelectMany(a => { try { return a.GetTypes(); } catch { return new Type[0]; } }) - .Where(t => t.GetCustomAttribute() != null); + .ToList(); + // Discover tools + var toolTypes = allTypes.Where(t => t.GetCustomAttribute() != null); + int toolCount = 0; foreach (var type in toolTypes) { - RegisterToolType(type); + if (RegisterCommandType(type, isResource: false)) + toolCount++; } - McpLog.Info($"Auto-discovered {_handlers.Count} tools"); + // Discover resources + var resourceTypes = allTypes.Where(t => t.GetCustomAttribute() != null); + int resourceCount = 0; + foreach (var type in resourceTypes) + { + if (RegisterCommandType(type, isResource: true)) + resourceCount++; + } + + McpLog.Info($"Auto-discovered {toolCount} tools and {resourceCount} resources ({_handlers.Count} total handlers)"); } catch (Exception ex) { - McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}"); + McpLog.Error($"Failed to auto-discover MCP commands: {ex.Message}"); } } - private static void RegisterToolType(Type type) + /// + /// Register a command type (tool or resource) with the registry. + /// Returns true if successfully registered, false otherwise. + /// + private static bool RegisterCommandType(Type type, bool isResource) { - var attr = type.GetCustomAttribute(); + string commandName; + string typeLabel = isResource ? "resource" : "tool"; + + // Get command name from appropriate attribute + if (isResource) + { + var resourceAttr = type.GetCustomAttribute(); + commandName = resourceAttr.ResourceName; + } + else + { + var toolAttr = type.GetCustomAttribute(); + commandName = toolAttr.CommandName; + } - // Get command name (explicit or auto-generated) - string commandName = attr.CommandName; + // Auto-generate command name if not explicitly provided if (string.IsNullOrEmpty(commandName)) { commandName = ToSnakeCase(type.Name); @@ -85,7 +117,7 @@ private static void RegisterToolType(Type type) { McpLog.Warn( $"Duplicate command name '{commandName}' detected. " + - $"Tool {type.Name} will override previously registered handler." + $"{typeLabel} {type.Name} will override previously registered handler." ); } @@ -101,10 +133,10 @@ private static void RegisterToolType(Type type) if (method == null) { McpLog.Warn( - $"MCP tool {type.Name} is marked with [McpForUnityTool] " + + $"MCP {typeLabel} {type.Name} is marked with [McpForUnity{(isResource ? "Resource" : "Tool")}] " + $"but has no public static HandleCommand(JObject) method" ); - return; + return false; } try @@ -114,10 +146,12 @@ private static void RegisterToolType(Type type) method ); _handlers[commandName] = handler; + return true; } catch (Exception ex) { - McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}"); + McpLog.Error($"Failed to register {typeLabel} {type.Name}: {ex.Message}"); + return false; } } From 8f470a71a8383d958425e529355269591dc0a8ec Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 15:21:28 -0400 Subject: [PATCH 21/42] Run get tests as a coroutine because it doesn't return results immediately This works but it spams logs like crazy, maybe there's a better/simpler way --- .../Editor/Helpers/EditorCoroutineExecutor.cs | 140 ++++++++++++++++++ .../Helpers/EditorCoroutineExecutor.cs.meta | 11 ++ MCPForUnity/Editor/MCPForUnityBridge.cs | 34 ++++- .../Editor/Resources/Tests/GetTests.cs | 136 ++++++++++------- MCPForUnity/Editor/Tools/CommandRegistry.cs | 86 +++++++++++ 5 files changed, 353 insertions(+), 54 deletions(-) create mode 100644 MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs create mode 100644 MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta diff --git a/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs b/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs new file mode 100644 index 00000000..14f635a0 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEditor; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Executes coroutines in the Unity Editor using EditorApplication.update. + /// This allows async-style operations that span multiple frames. + /// Shared utility used by both Tools and Resources for async command execution. + /// + public static class EditorCoroutineExecutor + { + private class CoroutineHandle + { + public IEnumerator Enumerator; + public Action OnComplete; + public Action OnError; + public bool IsComplete; + public object Result; + } + + private static List activeCoroutines = new List(); + private static bool isInitialized = false; + + /// + /// Start a coroutine that runs in the editor. + /// + /// The coroutine to execute + /// Called when coroutine completes with the final yielded value + /// Called if coroutine throws an exception + public static void StartCoroutine( + IEnumerator routine, + Action onComplete = null, + Action onError = null) + { + if (routine == null) + { + onError?.Invoke(new ArgumentNullException(nameof(routine))); + return; + } + + EnsureInitialized(); + + var handle = new CoroutineHandle + { + Enumerator = routine, + OnComplete = onComplete, + OnError = onError, + IsComplete = false + }; + + lock (activeCoroutines) + { + activeCoroutines.Add(handle); + } + } + + private static void EnsureInitialized() + { + if (isInitialized) return; + + EditorApplication.update += Update; + isInitialized = true; + } + + private static void Update() + { + if (activeCoroutines.Count == 0) return; + + // Snapshot to avoid modification during iteration + List toProcess; + lock (activeCoroutines) + { + toProcess = new List(activeCoroutines); + } + + var completedCoroutines = new List(); + + foreach (var handle in toProcess) + { + if (handle.IsComplete) continue; + + try + { + bool hasMore = handle.Enumerator.MoveNext(); + + if (hasMore) + { + // Store the current value as potential result + handle.Result = handle.Enumerator.Current; + } + else + { + // Coroutine completed + handle.IsComplete = true; + completedCoroutines.Add(handle); + handle.OnComplete?.Invoke(handle.Result); + } + } + catch (Exception ex) + { + handle.IsComplete = true; + completedCoroutines.Add(handle); + + // Wrap exception with more context + var wrappedException = new Exception( + $"Exception in coroutine MoveNext(): {ex.Message}", + ex + ); + handle.OnError?.Invoke(wrappedException); + } + } + + // Remove completed coroutines + if (completedCoroutines.Count > 0) + { + lock (activeCoroutines) + { + foreach (var completed in completedCoroutines) + { + activeCoroutines.Remove(completed); + } + } + } + } + + /// + /// Stop all running coroutines. Called during cleanup. + /// + public static void StopAllCoroutines() + { + lock (activeCoroutines) + { + activeCoroutines.Clear(); + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta b/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta new file mode 100644 index 00000000..76e08a11 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1fcd51175d75d411d98124f56b00f833 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs b/MCPForUnity/Editor/MCPForUnityBridge.cs index 0a46243e..8d0c39ef 100644 --- a/MCPForUnity/Editor/MCPForUnityBridge.cs +++ b/MCPForUnity/Editor/MCPForUnityBridge.cs @@ -465,6 +465,9 @@ public static void Stop() try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } try { EditorApplication.quitting -= Stop; } catch { } + // Stop all running coroutines + Helpers.EditorCoroutineExecutor.StopAllCoroutines(); + if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped."); } @@ -894,8 +897,33 @@ private static void ProcessCommands() } else { - string responseJson = ExecuteCommand(command); - tcs.SetResult(responseJson); + // Use JObject for parameters as handlers expect this + JObject paramsObject = command.@params ?? new JObject(); + + // Execute command (may be sync or async) + object result = CommandRegistry.ExecuteCommand(command.type, paramsObject, tcs); + + // If result is null, it means async execution - TCS will be completed by coroutine + // In this case, DON'T remove from queue yet, DON'T complete TCS + if (result == null) + { + // Async command - coroutine will complete TCS + // Setup cleanup when TCS completes - schedule on next frame to avoid race conditions + string asyncCommandId = id; + _ = tcs.Task.ContinueWith(_ => + { + // Use EditorApplication.delayCall to schedule cleanup on main thread, next frame + EditorApplication.delayCall += () => + { + lock (lockObj) { commandQueue.Remove(asyncCommandId); } + }; + }); + continue; // Skip the queue removal below + } + + // Synchronous result - complete TCS now + var response = new { status = "success", result }; + tcs.SetResult(JsonConvert.SerializeObject(response)); } } catch (Exception ex) @@ -915,7 +943,7 @@ private static void ProcessCommands() tcs.SetResult(responseJson); } - // Remove quickly under lock + // Remove from queue (only for sync commands - async ones skip with 'continue' above) lock (lockObj) { commandQueue.Remove(id); } } } diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index c62e8ab5..3460fc73 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using Newtonsoft.Json.Linq; using UnityEditor; @@ -15,40 +16,46 @@ namespace MCPForUnity.Editor.Resources.Tests [McpForUnityResource("get_tests")] public static class GetTests { - public static object HandleCommand(JObject @params) + public static IEnumerator HandleCommand(JObject @params) { - try - { - string modeStr = @params?["mode"]?.ToString(); - TestMode? parsedMode = null; + string modeStr = @params?["mode"]?.ToString(); + TestMode? parsedMode = null; - if (!string.IsNullOrWhiteSpace(modeStr)) + if (!string.IsNullOrWhiteSpace(modeStr)) + { + if (!ModeParser.TryParse(modeStr, out parsedMode, out var error)) { - if (!ModeParser.TryParse(modeStr, out parsedMode, out var error)) - { - return Response.Error(error); - } + yield return Response.Error(error); + yield break; } + } - McpLog.Info( - parsedMode.HasValue - ? $"[GetTests] Retrieving tests for mode: {parsedMode.Value}" - : "[GetTests] Retrieving tests for all modes", - always: false - ); - - var tests = TestCollector.GetTests(parsedMode); - string message = parsedMode.HasValue - ? $"Retrieved {tests.Count} {parsedMode.Value} tests" - : $"Retrieved {tests.Count} tests"; + McpLog.Info( + parsedMode.HasValue + ? $"[GetTests] Retrieving tests for mode: {parsedMode.Value}" + : "[GetTests] Retrieving tests for all modes", + always: false + ); - return Response.Success(message, tests); + // Use coroutine version of GetTests + var testsCoroutine = TestCollector.GetTestsAsync(parsedMode); + while (testsCoroutine.MoveNext()) + { + yield return null; // Wait a frame } - catch (Exception e) + + var tests = testsCoroutine.Current as List>; + if (tests == null) { - McpLog.Error($"[GetTests] Error retrieving tests: {e.Message}"); - return Response.Error($"Error retrieving tests: {e.Message}"); + yield return Response.Error("Failed to retrieve tests"); + yield break; } + + string message = parsedMode.HasValue + ? $"Retrieved {tests.Count} {parsedMode.Value} tests" + : $"Retrieved {tests.Count} tests"; + + yield return Response.Success(message, tests); } } @@ -59,32 +66,39 @@ public static object HandleCommand(JObject @params) [McpForUnityResource("get_tests_for_mode")] public static class GetTestsForMode { - public static object HandleCommand(JObject @params) + public static IEnumerator HandleCommand(JObject @params) { - try + string modeStr = @params["mode"]?.ToString(); + if (string.IsNullOrEmpty(modeStr)) { - string modeStr = @params["mode"]?.ToString(); - if (string.IsNullOrEmpty(modeStr)) - { - return Response.Error("'mode' parameter is required"); - } + yield return Response.Error("'mode' parameter is required"); + yield break; + } - if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) - { - return Response.Error(parseError); - } + if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) + { + yield return Response.Error(parseError); + yield break; + } - McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}", always: false); + McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}", always: false); - var tests = TestCollector.GetTests(parsedMode); - string message = $"Retrieved {tests.Count} {parsedMode.Value} tests"; - return Response.Success(message, tests); + // Use coroutine version of GetTests + var testsCoroutine = TestCollector.GetTestsAsync(parsedMode); + while (testsCoroutine.MoveNext()) + { + yield return null; // Wait a frame } - catch (Exception e) + + var tests = testsCoroutine.Current as List>; + if (tests == null) { - McpLog.Error($"[GetTestsForMode] Error retrieving tests: {e.Message}"); - return Response.Error($"Error retrieving tests: {e.Message}"); + yield return Response.Error("Failed to retrieve tests"); + yield break; } + + string message = $"Retrieved {tests.Count} {parsedMode.Value} tests"; + yield return Response.Success(message, tests); } } @@ -92,7 +106,10 @@ internal static class TestCollector { private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode }; - internal static List> GetTests(TestMode? filterMode) + /// + /// Async coroutine version that waits for Unity's TestRunnerApi callbacks to complete. + /// + internal static IEnumerator GetTestsAsync(TestMode? filterMode) { var modesToQuery = filterMode.HasValue ? new[] { filterMode.Value } : AllModes; var tests = new List>(); @@ -104,16 +121,33 @@ internal static List> GetTests(TestMode? filterMode) { foreach (var mode in modesToQuery) { + bool callbackInvoked = false; + ITestAdaptor capturedRoot = null; + var filter = new Filter(); api.RetrieveTestList(mode, root => { - if (root == null) - { - return; - } - - CollectFromNode(root, mode, tests, seen, new List()); + capturedRoot = root; + callbackInvoked = true; }); + + // Wait for the callback to be invoked (max 100 frames) + int maxFrames = 100; + while (!callbackInvoked && maxFrames-- > 0) + { + yield return null; // Wait one frame + } + + if (!callbackInvoked) + { + McpLog.Warn($"[TestCollector] Timeout waiting for test retrieval callback for {mode}"); + continue; + } + + if (capturedRoot != null) + { + CollectFromNode(capturedRoot, mode, tests, seen, new List()); + } } } finally @@ -124,7 +158,7 @@ internal static List> GetTests(TestMode? filterMode) } } - return tests; + yield return tests; // Return the final result } private static void CollectFromNode( diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs index a93dbead..04f4d560 100644 --- a/MCPForUnity/Editor/Tools/CommandRegistry.cs +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs @@ -1,10 +1,13 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Resources; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace MCPForUnity.Editor.Tools @@ -168,5 +171,88 @@ public static Func GetHandler(string commandName) } return handler; } + + /// + /// Execute a command handler, supporting both synchronous and asynchronous (coroutine) handlers. + /// If the handler returns an IEnumerator, it will be executed as a coroutine. + /// + /// The command name to execute + /// Command parameters + /// TaskCompletionSource to complete when async operation finishes + /// The result for synchronous commands, or null for async commands (TCS will be completed later) + public static object ExecuteCommand(string commandName, JObject @params, TaskCompletionSource tcs) + { + var handler = GetHandler(commandName); + var result = handler(@params); + + // Check if the result is a coroutine (async command) + if (result is IEnumerator coroutine) + { + // Start the coroutine - it will complete the TCS when done + Helpers.EditorCoroutineExecutor.StartCoroutine( + coroutine, + onComplete: (finalResult) => + { + try + { + var response = new { status = "success", result = finalResult }; + string json = JsonConvert.SerializeObject(response); + + // Use TrySetResult to avoid exception if TCS already completed + if (!tcs.TrySetResult(json)) + { + McpLog.Warn($"TCS for async command '{commandName}' was already completed"); + } + } + catch (Exception ex) + { + McpLog.Error($"Error completing async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); + + // Try to set error response + var errorResponse = new + { + status = "error", + error = $"Error completing command: {ex.Message}", + command = commandName + }; + tcs.TrySetResult(JsonConvert.SerializeObject(errorResponse)); + } + }, + onError: (ex) => + { + try + { + McpLog.Error($"Error in async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); + var errorResponse = new + { + status = "error", + error = ex.Message, + command = commandName, + stackTrace = ex.StackTrace + }; + string json = JsonConvert.SerializeObject(errorResponse); + + // Use TrySetResult to avoid exception if TCS already completed + if (!tcs.TrySetResult(json)) + { + McpLog.Warn($"TCS for async command '{commandName}' was already completed when trying to report error"); + } + } + catch (Exception serializationEx) + { + // Last resort - just try to set a simple error + McpLog.Error($"Failed to serialize error response: {serializationEx.Message}"); + tcs.TrySetResult("{\"status\":\"error\",\"error\":\"Failed to complete command\"}"); + } + } + ); + + // Return null to signal async execution (TCS will be completed by coroutine) + return null; + } + + // Synchronous result - caller will complete TCS + return result; + } } } From 6c0271432fbc58d71090223daf6448b58c021651 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 19:54:46 -0400 Subject: [PATCH 22/42] refactor: migrate from coroutines to async/await for test retrieval and command execution --- .../Editor/Helpers/EditorCoroutineExecutor.cs | 140 --------- .../Helpers/EditorCoroutineExecutor.cs.meta | 11 - MCPForUnity/Editor/MCPForUnityBridge.cs | 69 +++-- .../Editor/Resources/Tests/GetTests.cs | 116 +++---- MCPForUnity/Editor/Tools/CommandRegistry.cs | 292 +++++++++++++----- 5 files changed, 325 insertions(+), 303 deletions(-) delete mode 100644 MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs delete mode 100644 MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta diff --git a/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs b/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs deleted file mode 100644 index 14f635a0..00000000 --- a/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEditor; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Executes coroutines in the Unity Editor using EditorApplication.update. - /// This allows async-style operations that span multiple frames. - /// Shared utility used by both Tools and Resources for async command execution. - /// - public static class EditorCoroutineExecutor - { - private class CoroutineHandle - { - public IEnumerator Enumerator; - public Action OnComplete; - public Action OnError; - public bool IsComplete; - public object Result; - } - - private static List activeCoroutines = new List(); - private static bool isInitialized = false; - - /// - /// Start a coroutine that runs in the editor. - /// - /// The coroutine to execute - /// Called when coroutine completes with the final yielded value - /// Called if coroutine throws an exception - public static void StartCoroutine( - IEnumerator routine, - Action onComplete = null, - Action onError = null) - { - if (routine == null) - { - onError?.Invoke(new ArgumentNullException(nameof(routine))); - return; - } - - EnsureInitialized(); - - var handle = new CoroutineHandle - { - Enumerator = routine, - OnComplete = onComplete, - OnError = onError, - IsComplete = false - }; - - lock (activeCoroutines) - { - activeCoroutines.Add(handle); - } - } - - private static void EnsureInitialized() - { - if (isInitialized) return; - - EditorApplication.update += Update; - isInitialized = true; - } - - private static void Update() - { - if (activeCoroutines.Count == 0) return; - - // Snapshot to avoid modification during iteration - List toProcess; - lock (activeCoroutines) - { - toProcess = new List(activeCoroutines); - } - - var completedCoroutines = new List(); - - foreach (var handle in toProcess) - { - if (handle.IsComplete) continue; - - try - { - bool hasMore = handle.Enumerator.MoveNext(); - - if (hasMore) - { - // Store the current value as potential result - handle.Result = handle.Enumerator.Current; - } - else - { - // Coroutine completed - handle.IsComplete = true; - completedCoroutines.Add(handle); - handle.OnComplete?.Invoke(handle.Result); - } - } - catch (Exception ex) - { - handle.IsComplete = true; - completedCoroutines.Add(handle); - - // Wrap exception with more context - var wrappedException = new Exception( - $"Exception in coroutine MoveNext(): {ex.Message}", - ex - ); - handle.OnError?.Invoke(wrappedException); - } - } - - // Remove completed coroutines - if (completedCoroutines.Count > 0) - { - lock (activeCoroutines) - { - foreach (var completed in completedCoroutines) - { - activeCoroutines.Remove(completed); - } - } - } - } - - /// - /// Stop all running coroutines. Called during cleanup. - /// - public static void StopAllCoroutines() - { - lock (activeCoroutines) - { - activeCoroutines.Clear(); - } - } - } -} diff --git a/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta b/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta deleted file mode 100644 index 76e08a11..00000000 --- a/MCPForUnity/Editor/Helpers/EditorCoroutineExecutor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1fcd51175d75d411d98124f56b00f833 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs b/MCPForUnity/Editor/MCPForUnityBridge.cs index 8d0c39ef..6c87e662 100644 --- a/MCPForUnity/Editor/MCPForUnityBridge.cs +++ b/MCPForUnity/Editor/MCPForUnityBridge.cs @@ -19,6 +19,26 @@ namespace MCPForUnity.Editor { + + /// + /// Outbound message structure for the writer thread + /// + class Outbound + { + public byte[] Payload; + public string Tag; + public int? ReqId; + } + + /// + /// Queued command structure for main thread processing + /// + class QueuedCommand + { + public string CommandJson; + public TaskCompletionSource Tcs; + public bool IsExecuting; + } [InitializeOnLoad] public static partial class MCPForUnityBridge { @@ -28,13 +48,6 @@ public static partial class MCPForUnityBridge private static readonly object startStopLock = new(); private static readonly object clientsLock = new(); private static readonly System.Collections.Generic.HashSet activeClients = new(); - // Single-writer outbox for framed responses - private class Outbound - { - public byte[] Payload; - public string Tag; - public int? ReqId; - } private static readonly BlockingCollection _outbox = new(new ConcurrentQueue()); private static CancellationTokenSource cts; private static Task listenerTask; @@ -45,10 +58,7 @@ private class Outbound private static double nextStartAt = 0.0f; private static double nextHeartbeatAt = 0.0f; private static int heartbeatSeq = 0; - private static Dictionary< - string, - (string commandJson, TaskCompletionSource tcs) - > commandQueue = new(); + private static Dictionary commandQueue = new(); private static int mainThreadId; private static int currentUnityPort = 6400; // Dynamic port, starts with default private static bool isAutoConnectMode = false; @@ -465,9 +475,6 @@ public static void Stop() try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } try { EditorApplication.quitting -= Stop; } catch { } - // Stop all running coroutines - Helpers.EditorCoroutineExecutor.StopAllCoroutines(); - if (IsDebugEnabled()) McpLog.Info("MCPForUnityBridge stopped."); } @@ -588,7 +595,12 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken lock (lockObj) { - commandQueue[commandId] = (commandText, tcs); + commandQueue[commandId] = new QueuedCommand + { + CommandJson = commandText, + Tcs = tcs, + IsExecuting = false + }; } // Wait for the handler to produce a response, but do not block indefinitely @@ -820,19 +832,25 @@ private static void ProcessCommands() } // Snapshot under lock, then process outside to reduce contention - List<(string id, string text, TaskCompletionSource tcs)> work; + List<(string id, QueuedCommand command)> work; lock (lockObj) { - work = commandQueue - .Select(kvp => (kvp.Key, kvp.Value.commandJson, kvp.Value.tcs)) - .ToList(); + work = new List<(string, QueuedCommand)>(commandQueue.Count); + foreach (var kvp in commandQueue) + { + var queued = kvp.Value; + if (queued.IsExecuting) continue; + queued.IsExecuting = true; + work.Add((kvp.Key, queued)); + } } foreach (var item in work) { string id = item.id; - string commandText = item.text; - TaskCompletionSource tcs = item.tcs; + QueuedCommand queuedCommand = item.command; + string commandText = queuedCommand.CommandJson; + TaskCompletionSource tcs = queuedCommand.Tcs; try { @@ -903,11 +921,11 @@ private static void ProcessCommands() // Execute command (may be sync or async) object result = CommandRegistry.ExecuteCommand(command.type, paramsObject, tcs); - // If result is null, it means async execution - TCS will be completed by coroutine + // If result is null, it means async execution - TCS will be completed by the awaited task // In this case, DON'T remove from queue yet, DON'T complete TCS if (result == null) { - // Async command - coroutine will complete TCS + // Async command - the task continuation will complete the TCS // Setup cleanup when TCS completes - schedule on next frame to avoid race conditions string asyncCommandId = id; _ = tcs.Task.ContinueWith(_ => @@ -915,7 +933,10 @@ private static void ProcessCommands() // Use EditorApplication.delayCall to schedule cleanup on main thread, next frame EditorApplication.delayCall += () => { - lock (lockObj) { commandQueue.Remove(asyncCommandId); } + lock (lockObj) + { + commandQueue.Remove(asyncCommandId); + } }; }); continue; // Skip the queue removal below diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index 3460fc73..5ed9f94d 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -1,6 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; @@ -16,7 +16,7 @@ namespace MCPForUnity.Editor.Resources.Tests [McpForUnityResource("get_tests")] public static class GetTests { - public static IEnumerator HandleCommand(JObject @params) + public static async Task HandleCommand(JObject @params) { string modeStr = @params?["mode"]?.ToString(); TestMode? parsedMode = null; @@ -25,8 +25,7 @@ public static IEnumerator HandleCommand(JObject @params) { if (!ModeParser.TryParse(modeStr, out parsedMode, out var error)) { - yield return Response.Error(error); - yield break; + return Response.Error(error); } } @@ -37,25 +36,18 @@ public static IEnumerator HandleCommand(JObject @params) always: false ); - // Use coroutine version of GetTests - var testsCoroutine = TestCollector.GetTestsAsync(parsedMode); - while (testsCoroutine.MoveNext()) - { - yield return null; // Wait a frame - } + var tests = await TestCollector.GetTestsAsync(parsedMode).ConfigureAwait(true); - var tests = testsCoroutine.Current as List>; if (tests == null) { - yield return Response.Error("Failed to retrieve tests"); - yield break; + return Response.Error("Failed to retrieve tests"); } string message = parsedMode.HasValue ? $"Retrieved {tests.Count} {parsedMode.Value} tests" : $"Retrieved {tests.Count} tests"; - yield return Response.Success(message, tests); + return Response.Success(message, tests); } } @@ -66,39 +58,30 @@ public static IEnumerator HandleCommand(JObject @params) [McpForUnityResource("get_tests_for_mode")] public static class GetTestsForMode { - public static IEnumerator HandleCommand(JObject @params) + public static async Task HandleCommand(JObject @params) { string modeStr = @params["mode"]?.ToString(); if (string.IsNullOrEmpty(modeStr)) { - yield return Response.Error("'mode' parameter is required"); - yield break; + return Response.Error("'mode' parameter is required"); } if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) { - yield return Response.Error(parseError); - yield break; + return Response.Error(parseError); } McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}", always: false); - // Use coroutine version of GetTests - var testsCoroutine = TestCollector.GetTestsAsync(parsedMode); - while (testsCoroutine.MoveNext()) - { - yield return null; // Wait a frame - } + var tests = await TestCollector.GetTestsAsync(parsedMode).ConfigureAwait(true); - var tests = testsCoroutine.Current as List>; if (tests == null) { - yield return Response.Error("Failed to retrieve tests"); - yield break; + return Response.Error("Failed to retrieve tests"); } string message = $"Retrieved {tests.Count} {parsedMode.Value} tests"; - yield return Response.Success(message, tests); + return Response.Success(message, tests); } } @@ -107,9 +90,9 @@ internal static class TestCollector private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode }; /// - /// Async coroutine version that waits for Unity's TestRunnerApi callbacks to complete. + /// Retrieves tests asynchronously by awaiting Unity's TestRunnerApi callback. /// - internal static IEnumerator GetTestsAsync(TestMode? filterMode) + internal static async Task>> GetTestsAsync(TestMode? filterMode) { var modesToQuery = filterMode.HasValue ? new[] { filterMode.Value } : AllModes; var tests = new List>(); @@ -121,32 +104,10 @@ internal static IEnumerator GetTestsAsync(TestMode? filterMode) { foreach (var mode in modesToQuery) { - bool callbackInvoked = false; - ITestAdaptor capturedRoot = null; - - var filter = new Filter(); - api.RetrieveTestList(mode, root => + var root = await RetrieveTestRootAsync(api, mode).ConfigureAwait(true); + if (root != null) { - capturedRoot = root; - callbackInvoked = true; - }); - - // Wait for the callback to be invoked (max 100 frames) - int maxFrames = 100; - while (!callbackInvoked && maxFrames-- > 0) - { - yield return null; // Wait one frame - } - - if (!callbackInvoked) - { - McpLog.Warn($"[TestCollector] Timeout waiting for test retrieval callback for {mode}"); - continue; - } - - if (capturedRoot != null) - { - CollectFromNode(capturedRoot, mode, tests, seen, new List()); + CollectFromNode(root, mode, tests, seen, new List()); } } } @@ -158,7 +119,48 @@ internal static IEnumerator GetTestsAsync(TestMode? filterMode) } } - yield return tests; // Return the final result + return tests; + } + + private static async Task RetrieveTestRootAsync(TestRunnerApi api, TestMode mode) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + bool callbackInvoked = false; + + api.RetrieveTestList(mode, root => + { + callbackInvoked = true; + tcs.TrySetResult(root); + }); + + int framesRemaining = 100; + while (!callbackInvoked && framesRemaining-- > 0) + { + await WaitForNextEditorFrame().ConfigureAwait(true); + } + + if (!callbackInvoked) + { + McpLog.Warn($"[TestCollector] Timeout waiting for test retrieval callback for {mode}"); + return null; + } + + try + { + return await tcs.Task.ConfigureAwait(true); + } + catch (Exception ex) + { + McpLog.Error($"[TestCollector] Error retrieving tests for {mode}: {ex.Message}\n{ex.StackTrace}"); + return null; + } + } + + private static Task WaitForNextEditorFrame() + { + var frameTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + EditorApplication.delayCall += () => frameTcs.TrySetResult(true); + return frameTcs.Task; } private static void CollectFromNode( diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs index 04f4d560..406f84e2 100644 --- a/MCPForUnity/Editor/Tools/CommandRegistry.cs +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -12,13 +11,32 @@ namespace MCPForUnity.Editor.Tools { + /// + /// Holds information about a registered command handler. + /// + class HandlerInfo + { + public string CommandName { get; } + public Func SyncHandler { get; } + public Func> AsyncHandler { get; } + + public bool IsAsync => AsyncHandler != null; + + public HandlerInfo(string commandName, Func syncHandler, Func> asyncHandler) + { + CommandName = commandName; + SyncHandler = syncHandler; + AsyncHandler = asyncHandler; + } + } + /// /// Registry for all MCP command handlers via reflection. /// Handles both MCP tools and resources. /// public static class CommandRegistry { - private static readonly Dictionary> _handlers = new(); + private static readonly Dictionary _handlers = new(); private static bool _initialized = false; /// @@ -144,11 +162,23 @@ private static bool RegisterCommandType(Type type, bool isResource) try { - var handler = (Func)Delegate.CreateDelegate( - typeof(Func), - method - ); - _handlers[commandName] = handler; + HandlerInfo handlerInfo; + + if (typeof(Task).IsAssignableFrom(method.ReturnType)) + { + var asyncHandler = CreateAsyncHandlerDelegate(method, commandName); + handlerInfo = new HandlerInfo(commandName, null, asyncHandler); + } + else + { + var handler = (Func)Delegate.CreateDelegate( + typeof(Func), + method + ); + handlerInfo = new HandlerInfo(commandName, handler, null); + } + + _handlers[commandName] = handlerInfo; return true; } catch (Exception ex) @@ -161,7 +191,7 @@ private static bool RegisterCommandType(Type type, bool isResource) /// /// Get a command handler by name /// - public static Func GetHandler(string commandName) + private static HandlerInfo GetHandlerInfo(string commandName) { if (!_handlers.TryGetValue(commandName, out var handler)) { @@ -172,6 +202,26 @@ public static Func GetHandler(string commandName) return handler; } + /// + /// Get a synchronous command handler by name. + /// Throws if the command is asynchronous. + /// + /// + /// + /// + public static Func GetHandler(string commandName) + { + var handlerInfo = GetHandlerInfo(commandName); + if (handlerInfo.IsAsync) + { + throw new InvalidOperationException( + $"Command '{commandName}' is asynchronous and must be executed via ExecuteCommand" + ); + } + + return handlerInfo.SyncHandler; + } + /// /// Execute a command handler, supporting both synchronous and asynchronous (coroutine) handlers. /// If the handler returns an IEnumerator, it will be executed as a coroutine. @@ -182,77 +232,177 @@ public static Func GetHandler(string commandName) /// The result for synchronous commands, or null for async commands (TCS will be completed later) public static object ExecuteCommand(string commandName, JObject @params, TaskCompletionSource tcs) { - var handler = GetHandler(commandName); - var result = handler(@params); + var handlerInfo = GetHandlerInfo(commandName); - // Check if the result is a coroutine (async command) - if (result is IEnumerator coroutine) + if (handlerInfo.IsAsync) { - // Start the coroutine - it will complete the TCS when done - Helpers.EditorCoroutineExecutor.StartCoroutine( - coroutine, - onComplete: (finalResult) => - { - try - { - var response = new { status = "success", result = finalResult }; - string json = JsonConvert.SerializeObject(response); - - // Use TrySetResult to avoid exception if TCS already completed - if (!tcs.TrySetResult(json)) - { - McpLog.Warn($"TCS for async command '{commandName}' was already completed"); - } - } - catch (Exception ex) - { - McpLog.Error($"Error completing async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); - - // Try to set error response - var errorResponse = new - { - status = "error", - error = $"Error completing command: {ex.Message}", - command = commandName - }; - tcs.TrySetResult(JsonConvert.SerializeObject(errorResponse)); - } - }, - onError: (ex) => + ExecuteAsyncHandler(handlerInfo, @params, commandName, tcs); + return null; + } + + if (handlerInfo.SyncHandler == null) + { + throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation"); + } + + return handlerInfo.SyncHandler(@params); + } + + /// + /// Create a delegate for an async handler method that returns Task or Task. + /// The delegate will invoke the method and await its completion, returning the result. + /// + /// + /// + /// + /// + private static Func> CreateAsyncHandlerDelegate(MethodInfo method, string commandName) + { + return async (JObject parameters) => + { + object rawResult; + + try + { + rawResult = method.Invoke(null, new object[] { parameters }); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException ?? ex; + } + + if (rawResult == null) + { + return null; + } + + if (rawResult is not Task task) + { + throw new InvalidOperationException( + $"Async handler '{commandName}' returned an object that is not a Task" + ); + } + + await task.ConfigureAwait(true); + + var taskType = task.GetType(); + if (taskType.IsGenericType) + { + var resultProperty = taskType.GetProperty("Result"); + if (resultProperty != null) { - try - { - McpLog.Error($"Error in async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); - var errorResponse = new - { - status = "error", - error = ex.Message, - command = commandName, - stackTrace = ex.StackTrace - }; - string json = JsonConvert.SerializeObject(errorResponse); - - // Use TrySetResult to avoid exception if TCS already completed - if (!tcs.TrySetResult(json)) - { - McpLog.Warn($"TCS for async command '{commandName}' was already completed when trying to report error"); - } - } - catch (Exception serializationEx) - { - // Last resort - just try to set a simple error - McpLog.Error($"Failed to serialize error response: {serializationEx.Message}"); - tcs.TrySetResult("{\"status\":\"error\",\"error\":\"Failed to complete command\"}"); - } + return resultProperty.GetValue(task); } - ); + } - // Return null to signal async execution (TCS will be completed by coroutine) return null; + }; + } + + private static void ExecuteAsyncHandler( + HandlerInfo handlerInfo, + JObject parameters, + string commandName, + TaskCompletionSource tcs) + { + if (handlerInfo.AsyncHandler == null) + { + throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly"); + } + + Task handlerTask; + + try + { + handlerTask = handlerInfo.AsyncHandler(parameters); + } + catch (Exception ex) + { + ReportAsyncFailure(commandName, tcs, ex); + return; + } + + if (handlerTask == null) + { + CompleteAsyncCommand(commandName, tcs, null); + return; + } + + async void AwaitHandler() + { + try + { + var finalResult = await handlerTask.ConfigureAwait(true); + CompleteAsyncCommand(commandName, tcs, finalResult); + } + catch (Exception ex) + { + ReportAsyncFailure(commandName, tcs, ex); + } + } + + AwaitHandler(); + } + + /// + /// Complete the TaskCompletionSource for an async command with a success result. + /// + /// + /// + /// + private static void CompleteAsyncCommand(string commandName, TaskCompletionSource tcs, object result) + { + try + { + var response = new { status = "success", result }; + string json = JsonConvert.SerializeObject(response); + + if (!tcs.TrySetResult(json)) + { + McpLog.Warn($"TCS for async command '{commandName}' was already completed"); + } } + catch (Exception ex) + { + McpLog.Error($"Error completing async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); + ReportAsyncFailure(commandName, tcs, ex); + } + } - // Synchronous result - caller will complete TCS - return result; + /// + /// Report an error that occurred during async command execution. + /// Completes the TaskCompletionSource with an error response. + /// + /// + /// + /// + private static void ReportAsyncFailure(string commandName, TaskCompletionSource tcs, Exception ex) + { + McpLog.Error($"Error in async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); + + var errorResponse = new + { + status = "error", + error = ex.Message, + command = commandName, + stackTrace = ex.StackTrace + }; + + string json; + try + { + json = JsonConvert.SerializeObject(errorResponse); + } + catch (Exception serializationEx) + { + McpLog.Error($"Failed to serialize error response for '{commandName}': {serializationEx.Message}"); + json = "{\"status\":\"error\",\"error\":\"Failed to complete command\"}"; + } + + if (!tcs.TrySetResult(json)) + { + McpLog.Warn($"TCS for async command '{commandName}' was already completed when trying to report error"); + } } } } From 990903c9ad791fdc7f2d1a5c116f7d203222ab98 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 19:59:03 -0400 Subject: [PATCH 23/42] feat: add optional error field to MCPResponse model --- MCPForUnity/UnityMcpServer~/src/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MCPForUnity/UnityMcpServer~/src/models.py b/MCPForUnity/UnityMcpServer~/src/models.py index 6aa8a5f9..7a8c13ec 100644 --- a/MCPForUnity/UnityMcpServer~/src/models.py +++ b/MCPForUnity/UnityMcpServer~/src/models.py @@ -5,4 +5,5 @@ class MCPResponse(BaseModel): success: bool message: str + error: str | None = None data: Any | None = None From f1a96475136bd88164d5fa7aa7c66ab179b500c1 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 20:06:40 -0400 Subject: [PATCH 24/42] Increased timeout because loading tests can take some time --- MCPForUnity/UnityMcpServer~/src/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/config.py b/MCPForUnity/UnityMcpServer~/src/config.py index 526522da..0d829a9d 100644 --- a/MCPForUnity/UnityMcpServer~/src/config.py +++ b/MCPForUnity/UnityMcpServer~/src/config.py @@ -17,7 +17,7 @@ class ServerConfig: # Connection settings # short initial timeout; retries use shorter timeouts - connection_timeout: float = 1.0 + connection_timeout: float = 30.0 buffer_size: int = 16 * 1024 * 1024 # 16MB buffer # Framed receive behavior # max seconds to wait while consuming heartbeats only From 8c532886a84f5743e97beb0bc4bbb4dd8e578df2 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 20:51:44 -0400 Subject: [PATCH 25/42] Make message optional so error responses that only have success and error don't cause Pydantic errors --- MCPForUnity/UnityMcpServer~/src/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/models.py b/MCPForUnity/UnityMcpServer~/src/models.py index 7a8c13ec..cf1d33da 100644 --- a/MCPForUnity/UnityMcpServer~/src/models.py +++ b/MCPForUnity/UnityMcpServer~/src/models.py @@ -4,6 +4,6 @@ class MCPResponse(BaseModel): success: bool - message: str + message: str | None = None error: str | None = None data: Any | None = None From 45bf3328bd00c789e325f10c06f75902a7c52089 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 21:21:18 -0400 Subject: [PATCH 26/42] Set max_retries to 5 This connection module needs a lookover. The retries should be an exponential backoff and we could structure why it's failing so much --- MCPForUnity/UnityMcpServer~/src/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/config.py b/MCPForUnity/UnityMcpServer~/src/config.py index 0d829a9d..fa2fe377 100644 --- a/MCPForUnity/UnityMcpServer~/src/config.py +++ b/MCPForUnity/UnityMcpServer~/src/config.py @@ -30,7 +30,7 @@ class ServerConfig: log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" # Server settings - max_retries: int = 10 + max_retries: int = 5 retry_delay: float = 0.25 # Backoff hint returned to clients when Unity is reloading (milliseconds) reload_retry_ms: int = 250 From 0a267e707abb09f5adcfbc2ac11f5c51f2148e4a Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 21:22:48 -0400 Subject: [PATCH 27/42] Use pydantic model to structure the error output --- .../UnityMcpServer~/src/unity_connection.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/unity_connection.py b/MCPForUnity/UnityMcpServer~/src/unity_connection.py index 184ac917..f0e06b76 100644 --- a/MCPForUnity/UnityMcpServer~/src/unity_connection.py +++ b/MCPForUnity/UnityMcpServer~/src/unity_connection.py @@ -13,6 +13,8 @@ import time from typing import Any, Dict +from models import MCPResponse + # Configure logging using settings from config logging.basicConfig( @@ -227,8 +229,7 @@ def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict if not command_type: raise ValueError("MCP call missing command_type") if params is None: - # Return a fast, structured error that clients can display without hanging - return {"success": False, "error": "MCP call received with no parameters (client placeholder?)"} + return MCPResponse(success=False, error="MCP call received with no parameters (client placeholder?)") attempts = max(config.max_retries, 5) base_backoff = max(0.5, config.retry_delay) @@ -250,13 +251,12 @@ def read_status_file() -> dict | None: try: status = read_status_file() if status and (status.get('reloading') or status.get('reason') == 'reloading'): - return { - "success": False, - "state": "reloading", - "retry_after_ms": int(config.reload_retry_ms), - "error": "Unity domain reload in progress", - "message": "Unity is reloading scripts; please retry shortly" - } + return MCPResponse( + success=False, + error="Unity domain reload in progress, please try again shortly", + data={"state": "reloading", "retry_after_ms": int( + config.reload_retry_ms)} + ) except Exception: pass @@ -436,7 +436,7 @@ def send_command_with_retry(command_type: str, params: Dict[str, Any], *, max_re return response -async def async_send_command_with_retry(command_type: str, params: Dict[str, Any], *, loop=None, max_retries: int | None = None, retry_ms: int | None = None) -> Dict[str, Any]: +async def async_send_command_with_retry(command_type: str, params: dict[str, Any], *, loop=None, max_retries: int | None = None, retry_ms: int | None = None) -> dict[str, Any] | MCPResponse: """Async wrapper that runs the blocking retry helper in a thread pool.""" try: import asyncio # local import to avoid mandatory asyncio dependency for sync callers @@ -448,5 +448,4 @@ async def async_send_command_with_retry(command_type: str, params: Dict[str, Any command_type, params, max_retries=max_retries, retry_ms=retry_ms), ) except Exception as e: - # Return a structured error dict for consistency with other responses - return {"success": False, "error": f"Python async retry helper failed: {str(e)}"} + return MCPResponse(success=False, error=str(e)) From 8351d80383eececa0e075314ea15d11b84df2556 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 21:23:07 -0400 Subject: [PATCH 28/42] fix: initialize data field in GetTestsResponse to avoid potential errors --- MCPForUnity/UnityMcpServer~/src/resources/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tests.py b/MCPForUnity/UnityMcpServer~/src/resources/tests.py index fc182e2e..f5b7ab63 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/tests.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/tests.py @@ -15,18 +15,18 @@ class TestItem(BaseModel): class GetTestsResponse(MCPResponse): - data: list[TestItem] + data: list[TestItem] = [] @mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.") async def get_tests() -> GetTestsResponse: """Provides a list of all tests.""" response = await async_send_command_with_retry("get_tests", {}) - return GetTestsResponse(**response) + return GetTestsResponse(**response) if isinstance(response, dict) else response @mcp_for_unity_resource(uri="mcpforunity://tests/{mode}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.") async def get_tests_for_mode(mode: Annotated[Literal["edit", "play"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse: """Provides a list of tests for a specific mode.""" response = await async_send_command_with_retry("get_tests_for_mode", {"mode": mode}) - return GetTestsResponse(**response) + return GetTestsResponse(**response) if isinstance(response, dict) else response From d973f0d6d1c2235b6d9f50d03cdad2b43a4506d2 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 21:43:44 -0400 Subject: [PATCH 29/42] Don't return path parameter --- MCPForUnity/Editor/Resources/Tests/GetTests.cs | 12 +++--------- MCPForUnity/UnityMcpServer~/src/resources/tests.py | 7 +++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index 5ed9f94d..db89d749 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -191,12 +191,10 @@ List path if (!string.IsNullOrEmpty(fullName) && seen.Add(key)) { - string computedPath = path.Count > 0 ? string.Join("/", path) : fullName; output.Add(new Dictionary { ["name"] = node.Name ?? fullName, ["full_name"] = fullName, - ["path"] = computedPath, ["mode"] = mode.ToString(), }); } @@ -229,23 +227,19 @@ internal static bool TryParse(string modeStr, out TestMode? mode, out string err return false; } - if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase) || - modeStr.Equals("editmode", StringComparison.OrdinalIgnoreCase) || - modeStr.Equals("EditMode", StringComparison.Ordinal)) + if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase)) { mode = TestMode.EditMode; return true; } - if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase) || - modeStr.Equals("playmode", StringComparison.OrdinalIgnoreCase) || - modeStr.Equals("PlayMode", StringComparison.Ordinal)) + if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase)) { mode = TestMode.PlayMode; return true; } - error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'"; + error = $"Unknown test mode: '{modeStr}'. Use 'edit' or 'play'"; return false; } } diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tests.py b/MCPForUnity/UnityMcpServer~/src/resources/tests.py index f5b7ab63..4268a143 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/tests.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/tests.py @@ -9,9 +9,8 @@ class TestItem(BaseModel): name: Annotated[str, Field(description="The name of the test.")] full_name: Annotated[str, Field(description="The full name of the test.")] - path: Annotated[str, Field(description="The path of the test.")] - mode: Annotated[str, Field( - description="The mode the test is for (EditMode or PlayMode).")] + mode: Annotated[Literal["EditMode", "PlayMode"], + Field(description="The mode the test is for.")] class GetTestsResponse(MCPResponse): @@ -26,7 +25,7 @@ async def get_tests() -> GetTestsResponse: @mcp_for_unity_resource(uri="mcpforunity://tests/{mode}", name="get_tests_for_mode", description="Provides a list of tests for a specific mode.") -async def get_tests_for_mode(mode: Annotated[Literal["edit", "play"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse: +async def get_tests_for_mode(mode: Annotated[Literal["EditMode", "PlayMode"], Field(description="The mode to filter tests by.")]) -> GetTestsResponse: """Provides a list of tests for a specific mode.""" response = await async_send_command_with_retry("get_tests_for_mode", {"mode": mode}) return GetTestsResponse(**response) if isinstance(response, dict) else response From 0d55f66449616b4f9e023c54eee4658faec91603 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 23:37:30 -0400 Subject: [PATCH 30/42] feat: add Unity test runner execution with structured results and Python bindings --- MCPForUnity/Editor/Helpers/TestRunExecutor.cs | 237 ++++++++++++++++++ .../Editor/Helpers/TestRunExecutor.cs.meta | 11 + MCPForUnity/Editor/Tools/RunTests.cs | 74 ++++++ MCPForUnity/Editor/Tools/RunTests.cs.meta | 11 + .../UnityMcpServer~/src/tools/run_tests.py | 57 +++++ 5 files changed, 390 insertions(+) create mode 100644 MCPForUnity/Editor/Helpers/TestRunExecutor.cs create mode 100644 MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta create mode 100644 MCPForUnity/Editor/Tools/RunTests.cs create mode 100644 MCPForUnity/Editor/Tools/RunTests.cs.meta create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/run_tests.py diff --git a/MCPForUnity/Editor/Helpers/TestRunExecutor.cs b/MCPForUnity/Editor/Helpers/TestRunExecutor.cs new file mode 100644 index 00000000..39e885d8 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/TestRunExecutor.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.TestTools.TestRunner.Api; +using UnityEngine; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Executes Unity Test Runner suites and returns structured results. + /// + internal sealed class TestRunExecutor : ICallbacks, IDisposable + { + private readonly TestRunnerApi _testRunnerApi; + private readonly List _leafResults = new List(); + private TaskCompletionSource _completionSource; + + public TestRunExecutor() + { + _testRunnerApi = ScriptableObject.CreateInstance(); + _testRunnerApi.RegisterCallbacks(this); + } + + /// + /// Execute all tests for the provided mode. + /// + public Task RunTestsAsync(TestMode mode) + { + if (_completionSource != null && !_completionSource.Task.IsCompleted) + { + throw new InvalidOperationException("A test run is already in progress."); + } + + _leafResults.Clear(); + _completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var filter = new Filter { testMode = mode }; + _testRunnerApi.Execute(new ExecutionSettings(filter)); + + return _completionSource.Task; + } + + public void RunStarted(ITestAdaptor testsToRun) + { + _leafResults.Clear(); + } + + public void RunFinished(ITestResultAdaptor result) + { + if (_completionSource == null) + { + return; + } + + var payload = TestRunResult.Create(result, _leafResults); + _completionSource.TrySetResult(payload); + _completionSource = null; + } + + public void TestStarted(ITestAdaptor test) + { + // No-op: we only need the finished results + } + + public void TestFinished(ITestResultAdaptor result) + { + if (result == null) + { + return; + } + + if (!result.HasChildren) + { + _leafResults.Add(result); + } + } + + public void Dispose() + { + try + { + _testRunnerApi?.UnregisterCallbacks(this); + } + catch + { + // Best effort cleanup + } + + if (_testRunnerApi != null) + { + ScriptableObject.DestroyImmediate(_testRunnerApi); + } + } + + /// + /// Serializable result for a single test run. + /// + internal sealed class TestRunResult + { + private TestRunResult( + int total, + int passed, + int failed, + int skipped, + double durationSeconds, + string resultState, + IReadOnlyList tests) + { + Total = total; + Passed = passed; + Failed = failed; + Skipped = skipped; + DurationSeconds = durationSeconds; + ResultState = resultState; + Tests = tests; + } + + public int Total { get; } + public int Passed { get; } + public int Failed { get; } + public int Skipped { get; } + public double DurationSeconds { get; } + public string ResultState { get; } + public IReadOnlyList Tests { get; } + + public object ToSerializable(string mode) + { + return new + { + mode, + summary = new + { + total = Total, + passed = Passed, + failed = Failed, + skipped = Skipped, + durationSeconds = DurationSeconds, + resultState = ResultState, + }, + results = Tests.Select(t => t.ToSerializable()).ToList(), + }; + } + + public static TestRunResult Create(ITestResultAdaptor summary, IReadOnlyList testResults) + { + var serializedTests = testResults + .Select(TestRunTestResult.FromAdaptor) + .ToList(); + + int passed = summary?.PassCount + ?? serializedTests.Count(t => string.Equals(t.State, "Passed", StringComparison.OrdinalIgnoreCase)); + int failed = summary?.FailCount + ?? serializedTests.Count(t => string.Equals(t.State, "Failed", StringComparison.OrdinalIgnoreCase)); + int skipped = summary?.SkipCount + ?? serializedTests.Count(t => string.Equals(t.State, "Skipped", StringComparison.OrdinalIgnoreCase)); + + double duration = summary?.Duration + ?? serializedTests.Sum(t => t.DurationSeconds); + + int total = summary != null + ? passed + failed + skipped + : serializedTests.Count; + + return new TestRunResult( + total, + passed, + failed, + skipped, + duration, + summary?.ResultState ?? "Unknown", + serializedTests); + } + } + + internal sealed class TestRunTestResult + { + private TestRunTestResult( + string name, + string fullName, + string state, + double duration, + string message, + string stackTrace, + string output) + { + Name = name; + FullName = fullName; + State = state; + DurationSeconds = duration; + Message = message; + StackTrace = stackTrace; + Output = output; + } + + public string Name { get; } + public string FullName { get; } + public string State { get; } + public double DurationSeconds { get; } + public string Message { get; } + public string StackTrace { get; } + public string Output { get; } + + public object ToSerializable() + { + return new + { + name = Name, + fullName = FullName, + state = State, + durationSeconds = DurationSeconds, + message = Message, + stackTrace = StackTrace, + output = Output, + }; + } + + public static TestRunTestResult FromAdaptor(ITestResultAdaptor adaptor) + { + if (adaptor == null) + { + return new TestRunTestResult(string.Empty, string.Empty, "Unknown", 0.0, string.Empty, string.Empty, string.Empty); + } + + return new TestRunTestResult( + adaptor.Name, + adaptor.FullName, + adaptor.ResultState, + adaptor.Duration, + adaptor.Message, + adaptor.StackTrace, + adaptor.Output); + } + } + } +} diff --git a/MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta b/MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta new file mode 100644 index 00000000..432dab0e --- /dev/null +++ b/MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fa39ccbcfb3fb4678beed6d9de97b108 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs new file mode 100644 index 00000000..ebbbff75 --- /dev/null +++ b/MCPForUnity/Editor/Tools/RunTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading.Tasks; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Resources.Tests; +using Newtonsoft.Json.Linq; +using UnityEditor.TestTools.TestRunner.Api; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Executes Unity tests for a specified mode and returns detailed results. + /// + [McpForUnityTool("run_tests")] + public static class RunTests + { + private const int DefaultTimeoutSeconds = 600; // 10 minutes + + public static async Task HandleCommand(JObject @params) + { + string modeStr = @params?["mode"]?.ToString(); + if (string.IsNullOrWhiteSpace(modeStr)) + { + modeStr = "edit"; + } + + if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) + { + return Response.Error(parseError); + } + + int timeoutSeconds = DefaultTimeoutSeconds; + try + { + var timeoutToken = @params?["timeoutSeconds"]; + if (timeoutToken != null && int.TryParse(timeoutToken.ToString(), out var parsedTimeout) && parsedTimeout > 0) + { + timeoutSeconds = parsedTimeout; + } + } + catch + { + // Preserve default timeout if parsing fails + } + + using var executor = new TestRunExecutor(); + Task runTask; + + try + { + runTask = executor.RunTestsAsync(parsedMode.Value); + } + catch (Exception ex) + { + return Response.Error($"Failed to start test run: {ex.Message}"); + } + + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(timeoutSeconds)); + var completed = await Task.WhenAny(runTask, timeoutTask).ConfigureAwait(true); + + if (completed != runTask) + { + return Response.Error($"Test run timed out after {timeoutSeconds} seconds"); + } + + var result = await runTask.ConfigureAwait(true); + + string message = + $"{parsedMode.Value} tests completed: {result.Passed}/{result.Total} passed, {result.Failed} failed, {result.Skipped} skipped"; + + var data = result.ToSerializable(parsedMode.Value.ToString()); + return Response.Success(message, data); + } + } +} diff --git a/MCPForUnity/Editor/Tools/RunTests.cs.meta b/MCPForUnity/Editor/Tools/RunTests.cs.meta new file mode 100644 index 00000000..85d66e08 --- /dev/null +++ b/MCPForUnity/Editor/Tools/RunTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b177f204e300948f7ae07fb45d4c7ca9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py b/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py new file mode 100644 index 00000000..53199473 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py @@ -0,0 +1,57 @@ +"""Tool for executing Unity Test Runner suites.""" +from typing import Annotated, Literal, Any + +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from models import MCPResponse +from registry import mcp_for_unity_tool +from unity_connection import async_send_command_with_retry + + +class RunTestsSummary(BaseModel): + total: int + passed: int + failed: int + skipped: int + durationSeconds: float + resultState: str + + +class RunTestsTestResult(BaseModel): + name: str + fullName: str + state: str + durationSeconds: float + message: str | None = None + stackTrace: str | None = None + output: str | None = None + + +class RunTestsResult(BaseModel): + mode: str + summary: RunTestsSummary + results: list[RunTestsTestResult] + + +class RunTestsResponse(MCPResponse): + data: RunTestsResult | None = None + + +@mcp_for_unity_tool(description="Runs Unity tests for the specified mode") +async def run_tests( + ctx: Context, + mode: Annotated[Literal["edit", "play"], Field( + description="Unity test mode to run")] = "edit", + timeout_seconds: Annotated[int, Field( + description="Optional timeout in seconds for the Unity test run")] | None = None, +) -> RunTestsResponse: + await ctx.info(f"Processing run_tests: mode={mode}") + + params: dict[str, Any] = {"mode": mode} + if timeout_seconds is not None: + params["timeoutSeconds"] = timeout_seconds + + response = await async_send_command_with_retry("run_tests", params) + await ctx.info(f'Response {response}') + return RunTestsResponse(**response) if isinstance(response, dict) else response From 1427c905f8847505085344877bdb506dc09920c6 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sun, 12 Oct 2025 23:44:37 -0400 Subject: [PATCH 31/42] refactor: simplify GetTests by removing mode filtering and related parsing logic --- .../Editor/Resources/Tests/GetTests.cs | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index db89d749..fdb96702 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -18,34 +18,16 @@ public static class GetTests { public static async Task HandleCommand(JObject @params) { - string modeStr = @params?["mode"]?.ToString(); - TestMode? parsedMode = null; + McpLog.Info("[GetTests] Retrieving tests for all modes"); - if (!string.IsNullOrWhiteSpace(modeStr)) - { - if (!ModeParser.TryParse(modeStr, out parsedMode, out var error)) - { - return Response.Error(error); - } - } - - McpLog.Info( - parsedMode.HasValue - ? $"[GetTests] Retrieving tests for mode: {parsedMode.Value}" - : "[GetTests] Retrieving tests for all modes", - always: false - ); - - var tests = await TestCollector.GetTestsAsync(parsedMode).ConfigureAwait(true); + var tests = await TestCollector.GetTestsAsync(filterMode: null).ConfigureAwait(true); if (tests == null) { return Response.Error("Failed to retrieve tests"); } - string message = parsedMode.HasValue - ? $"Retrieved {tests.Count} {parsedMode.Value} tests" - : $"Retrieved {tests.Count} tests"; + string message = $"Retrieved {tests.Count} tests"; return Response.Success(message, tests); } From 5b78dd81b861990841163236b89b1367266f8d2d Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 00:23:15 -0400 Subject: [PATCH 32/42] refactor: move test runner functionality into dedicated service interface --- MCPForUnity/Editor/Helpers/TestRunExecutor.cs | 237 ----------- .../Editor/Resources/Tests/GetTests.cs | 148 +------ .../Editor/Services/ITestRunnerService.cs | 23 ++ .../ITestRunnerService.cs.meta} | 2 +- .../Editor/Services/MCPServiceLocator.cs | 16 + .../Editor/Services/TestRunnerService.cs | 387 ++++++++++++++++++ .../Editor/Services/TestRunnerService.cs.meta | 11 + MCPForUnity/Editor/Tools/RunTests.cs | 9 +- 8 files changed, 455 insertions(+), 378 deletions(-) delete mode 100644 MCPForUnity/Editor/Helpers/TestRunExecutor.cs create mode 100644 MCPForUnity/Editor/Services/ITestRunnerService.cs rename MCPForUnity/Editor/{Helpers/TestRunExecutor.cs.meta => Services/ITestRunnerService.cs.meta} (83%) create mode 100644 MCPForUnity/Editor/Services/TestRunnerService.cs create mode 100644 MCPForUnity/Editor/Services/TestRunnerService.cs.meta diff --git a/MCPForUnity/Editor/Helpers/TestRunExecutor.cs b/MCPForUnity/Editor/Helpers/TestRunExecutor.cs deleted file mode 100644 index 39e885d8..00000000 --- a/MCPForUnity/Editor/Helpers/TestRunExecutor.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using UnityEditor; -using UnityEditor.TestTools.TestRunner.Api; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Executes Unity Test Runner suites and returns structured results. - /// - internal sealed class TestRunExecutor : ICallbacks, IDisposable - { - private readonly TestRunnerApi _testRunnerApi; - private readonly List _leafResults = new List(); - private TaskCompletionSource _completionSource; - - public TestRunExecutor() - { - _testRunnerApi = ScriptableObject.CreateInstance(); - _testRunnerApi.RegisterCallbacks(this); - } - - /// - /// Execute all tests for the provided mode. - /// - public Task RunTestsAsync(TestMode mode) - { - if (_completionSource != null && !_completionSource.Task.IsCompleted) - { - throw new InvalidOperationException("A test run is already in progress."); - } - - _leafResults.Clear(); - _completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var filter = new Filter { testMode = mode }; - _testRunnerApi.Execute(new ExecutionSettings(filter)); - - return _completionSource.Task; - } - - public void RunStarted(ITestAdaptor testsToRun) - { - _leafResults.Clear(); - } - - public void RunFinished(ITestResultAdaptor result) - { - if (_completionSource == null) - { - return; - } - - var payload = TestRunResult.Create(result, _leafResults); - _completionSource.TrySetResult(payload); - _completionSource = null; - } - - public void TestStarted(ITestAdaptor test) - { - // No-op: we only need the finished results - } - - public void TestFinished(ITestResultAdaptor result) - { - if (result == null) - { - return; - } - - if (!result.HasChildren) - { - _leafResults.Add(result); - } - } - - public void Dispose() - { - try - { - _testRunnerApi?.UnregisterCallbacks(this); - } - catch - { - // Best effort cleanup - } - - if (_testRunnerApi != null) - { - ScriptableObject.DestroyImmediate(_testRunnerApi); - } - } - - /// - /// Serializable result for a single test run. - /// - internal sealed class TestRunResult - { - private TestRunResult( - int total, - int passed, - int failed, - int skipped, - double durationSeconds, - string resultState, - IReadOnlyList tests) - { - Total = total; - Passed = passed; - Failed = failed; - Skipped = skipped; - DurationSeconds = durationSeconds; - ResultState = resultState; - Tests = tests; - } - - public int Total { get; } - public int Passed { get; } - public int Failed { get; } - public int Skipped { get; } - public double DurationSeconds { get; } - public string ResultState { get; } - public IReadOnlyList Tests { get; } - - public object ToSerializable(string mode) - { - return new - { - mode, - summary = new - { - total = Total, - passed = Passed, - failed = Failed, - skipped = Skipped, - durationSeconds = DurationSeconds, - resultState = ResultState, - }, - results = Tests.Select(t => t.ToSerializable()).ToList(), - }; - } - - public static TestRunResult Create(ITestResultAdaptor summary, IReadOnlyList testResults) - { - var serializedTests = testResults - .Select(TestRunTestResult.FromAdaptor) - .ToList(); - - int passed = summary?.PassCount - ?? serializedTests.Count(t => string.Equals(t.State, "Passed", StringComparison.OrdinalIgnoreCase)); - int failed = summary?.FailCount - ?? serializedTests.Count(t => string.Equals(t.State, "Failed", StringComparison.OrdinalIgnoreCase)); - int skipped = summary?.SkipCount - ?? serializedTests.Count(t => string.Equals(t.State, "Skipped", StringComparison.OrdinalIgnoreCase)); - - double duration = summary?.Duration - ?? serializedTests.Sum(t => t.DurationSeconds); - - int total = summary != null - ? passed + failed + skipped - : serializedTests.Count; - - return new TestRunResult( - total, - passed, - failed, - skipped, - duration, - summary?.ResultState ?? "Unknown", - serializedTests); - } - } - - internal sealed class TestRunTestResult - { - private TestRunTestResult( - string name, - string fullName, - string state, - double duration, - string message, - string stackTrace, - string output) - { - Name = name; - FullName = fullName; - State = state; - DurationSeconds = duration; - Message = message; - StackTrace = stackTrace; - Output = output; - } - - public string Name { get; } - public string FullName { get; } - public string State { get; } - public double DurationSeconds { get; } - public string Message { get; } - public string StackTrace { get; } - public string Output { get; } - - public object ToSerializable() - { - return new - { - name = Name, - fullName = FullName, - state = State, - durationSeconds = DurationSeconds, - message = Message, - stackTrace = StackTrace, - output = Output, - }; - } - - public static TestRunTestResult FromAdaptor(ITestResultAdaptor adaptor) - { - if (adaptor == null) - { - return new TestRunTestResult(string.Empty, string.Empty, "Unknown", 0.0, string.Empty, string.Empty, string.Empty); - } - - return new TestRunTestResult( - adaptor.Name, - adaptor.FullName, - adaptor.ResultState, - adaptor.Duration, - adaptor.Message, - adaptor.StackTrace, - adaptor.Output); - } - } - } -} diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index fdb96702..0c8199fc 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -2,10 +2,9 @@ using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using UnityEditor.TestTools.TestRunner.Api; using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using UnityEditor.TestTools.TestRunner.Api; namespace MCPForUnity.Editor.Resources.Tests { @@ -20,7 +19,11 @@ public static async Task HandleCommand(JObject @params) { McpLog.Info("[GetTests] Retrieving tests for all modes"); - var tests = await TestCollector.GetTestsAsync(filterMode: null).ConfigureAwait(true); + var service = MCPServiceLocator.Tests; + var result = await service.GetTestsAsync(mode: null).ConfigureAwait(true); + var tests = result is List> list + ? list + : new List>(result); if (tests == null) { @@ -53,9 +56,13 @@ public static async Task HandleCommand(JObject @params) return Response.Error(parseError); } - McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}", always: false); + McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}"); - var tests = await TestCollector.GetTestsAsync(parsedMode).ConfigureAwait(true); + var service = MCPServiceLocator.Tests; + var result = await service.GetTestsAsync(parsedMode).ConfigureAwait(true); + var tests = result is List> list + ? list + : new List>(result); if (tests == null) { @@ -67,135 +74,6 @@ public static async Task HandleCommand(JObject @params) } } - internal static class TestCollector - { - private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode }; - - /// - /// Retrieves tests asynchronously by awaiting Unity's TestRunnerApi callback. - /// - internal static async Task>> GetTestsAsync(TestMode? filterMode) - { - var modesToQuery = filterMode.HasValue ? new[] { filterMode.Value } : AllModes; - var tests = new List>(); - var seen = new HashSet(StringComparer.Ordinal); - - var api = ScriptableObject.CreateInstance(); - - try - { - foreach (var mode in modesToQuery) - { - var root = await RetrieveTestRootAsync(api, mode).ConfigureAwait(true); - if (root != null) - { - CollectFromNode(root, mode, tests, seen, new List()); - } - } - } - finally - { - if (api != null) - { - ScriptableObject.DestroyImmediate(api); - } - } - - return tests; - } - - private static async Task RetrieveTestRootAsync(TestRunnerApi api, TestMode mode) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - bool callbackInvoked = false; - - api.RetrieveTestList(mode, root => - { - callbackInvoked = true; - tcs.TrySetResult(root); - }); - - int framesRemaining = 100; - while (!callbackInvoked && framesRemaining-- > 0) - { - await WaitForNextEditorFrame().ConfigureAwait(true); - } - - if (!callbackInvoked) - { - McpLog.Warn($"[TestCollector] Timeout waiting for test retrieval callback for {mode}"); - return null; - } - - try - { - return await tcs.Task.ConfigureAwait(true); - } - catch (Exception ex) - { - McpLog.Error($"[TestCollector] Error retrieving tests for {mode}: {ex.Message}\n{ex.StackTrace}"); - return null; - } - } - - private static Task WaitForNextEditorFrame() - { - var frameTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - EditorApplication.delayCall += () => frameTcs.TrySetResult(true); - return frameTcs.Task; - } - - private static void CollectFromNode( - ITestAdaptor node, - TestMode mode, - List> output, - HashSet seen, - List path - ) - { - if (node == null) - { - return; - } - - bool hasName = !string.IsNullOrEmpty(node.Name); - if (hasName) - { - path.Add(node.Name); - } - - bool hasChildren = node.HasChildren && node.Children != null; - - if (!hasChildren) - { - string fullName = string.IsNullOrEmpty(node.FullName) ? node.Name ?? string.Empty : node.FullName; - string key = $"{mode}:{fullName}"; - - if (!string.IsNullOrEmpty(fullName) && seen.Add(key)) - { - output.Add(new Dictionary - { - ["name"] = node.Name ?? fullName, - ["full_name"] = fullName, - ["mode"] = mode.ToString(), - }); - } - } - else - { - foreach (var child in node.Children) - { - CollectFromNode(child, mode, output, seen, path); - } - } - - if (hasName) - { - path.RemoveAt(path.Count - 1); - } - } - } - internal static class ModeParser { internal static bool TryParse(string modeStr, out TestMode? mode, out string error) diff --git a/MCPForUnity/Editor/Services/ITestRunnerService.cs b/MCPForUnity/Editor/Services/ITestRunnerService.cs new file mode 100644 index 00000000..575e4d93 --- /dev/null +++ b/MCPForUnity/Editor/Services/ITestRunnerService.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEditor.TestTools.TestRunner.Api; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Provides access to Unity Test Runner data and execution. + /// + public interface ITestRunnerService + { + /// + /// Retrieve the list of tests for the requested mode(s). + /// When is null, tests for both EditMode and PlayMode are returned. + /// + Task>> GetTestsAsync(TestMode? mode); + + /// + /// Execute tests for the supplied mode. + /// + Task RunTestsAsync(TestMode mode); + } +} diff --git a/MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta b/MCPForUnity/Editor/Services/ITestRunnerService.cs.meta similarity index 83% rename from MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta rename to MCPForUnity/Editor/Services/ITestRunnerService.cs.meta index 432dab0e..ea325e73 100644 --- a/MCPForUnity/Editor/Helpers/TestRunExecutor.cs.meta +++ b/MCPForUnity/Editor/Services/ITestRunnerService.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: fa39ccbcfb3fb4678beed6d9de97b108 +guid: d23bf32361ff444beaf3510818c94bae MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs index 547f9c23..96c92909 100644 --- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -1,3 +1,5 @@ +using System; + namespace MCPForUnity.Editor.Services { /// @@ -8,6 +10,7 @@ public static class MCPServiceLocator private static IBridgeControlService _bridgeService; private static IClientConfigurationService _clientService; private static IPathResolverService _pathService; + private static ITestRunnerService _testRunnerService; /// /// Gets the bridge control service @@ -24,6 +27,11 @@ public static class MCPServiceLocator /// public static IPathResolverService Paths => _pathService ??= new PathResolverService(); + /// + /// Gets the Unity test runner service + /// + public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService(); + /// /// Registers a custom implementation for a service (useful for testing) /// @@ -37,6 +45,8 @@ public static void Register(T implementation) where T : class _clientService = c; else if (implementation is IPathResolverService p) _pathService = p; + else if (implementation is ITestRunnerService t) + _testRunnerService = t; } /// @@ -44,9 +54,15 @@ public static void Register(T implementation) where T : class /// public static void Reset() { + (_bridgeService as IDisposable)?.Dispose(); + (_clientService as IDisposable)?.Dispose(); + (_pathService as IDisposable)?.Dispose(); + (_testRunnerService as IDisposable)?.Dispose(); + _bridgeService = null; _clientService = null; _pathService = null; + _testRunnerService = null; } } } diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs b/MCPForUnity/Editor/Services/TestRunnerService.cs new file mode 100644 index 00000000..60411278 --- /dev/null +++ b/MCPForUnity/Editor/Services/TestRunnerService.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEditor.TestTools.TestRunner.Api; +using UnityEngine; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Concrete implementation of . + /// Coordinates Unity Test Runner operations and produces structured results. + /// + internal sealed class TestRunnerService : ITestRunnerService, ICallbacks, IDisposable + { + private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode }; + + private readonly TestRunnerApi _testRunnerApi; + private readonly SemaphoreSlim _operationLock = new SemaphoreSlim(1, 1); + private readonly List _leafResults = new List(); + private TaskCompletionSource _runCompletionSource; + + public TestRunnerService() + { + _testRunnerApi = ScriptableObject.CreateInstance(); + _testRunnerApi.RegisterCallbacks(this); + } + + public async Task>> GetTestsAsync(TestMode? mode) + { + await _operationLock.WaitAsync().ConfigureAwait(false); + try + { + var modes = mode.HasValue ? new[] { mode.Value } : AllModes; + + var results = new List>(); + var seen = new HashSet(StringComparer.Ordinal); + + foreach (var m in modes) + { + var root = await RetrieveTestRootAsync(m).ConfigureAwait(true); + if (root != null) + { + CollectFromNode(root, m, results, seen, new List()); + } + } + + return results; + } + finally + { + _operationLock.Release(); + } + } + + public async Task RunTestsAsync(TestMode mode) + { + await _operationLock.WaitAsync().ConfigureAwait(false); + Task runTask; + try + { + if (_runCompletionSource != null && !_runCompletionSource.Task.IsCompleted) + { + throw new InvalidOperationException("A Unity test run is already in progress."); + } + + _leafResults.Clear(); + _runCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var filter = new Filter { testMode = mode }; + _testRunnerApi.Execute(new ExecutionSettings(filter)); + + runTask = _runCompletionSource.Task; + } + catch + { + _operationLock.Release(); + throw; + } + + try + { + return await runTask.ConfigureAwait(true); + } + finally + { + _operationLock.Release(); + } + } + + public void Dispose() + { + try + { + _testRunnerApi?.UnregisterCallbacks(this); + } + catch + { + // Ignore cleanup errors + } + + if (_testRunnerApi != null) + { + ScriptableObject.DestroyImmediate(_testRunnerApi); + } + + _operationLock.Dispose(); + } + + #region TestRunnerApi callbacks + + public void RunStarted(ITestAdaptor testsToRun) + { + _leafResults.Clear(); + } + + public void RunFinished(ITestResultAdaptor result) + { + if (_runCompletionSource == null) + { + return; + } + + var payload = TestRunResult.Create(result, _leafResults); + _runCompletionSource.TrySetResult(payload); + _runCompletionSource = null; + } + + public void TestStarted(ITestAdaptor test) + { + // No-op + } + + public void TestFinished(ITestResultAdaptor result) + { + if (result == null) + { + return; + } + + if (!result.HasChildren) + { + _leafResults.Add(result); + } + } + + #endregion + + #region Test list helpers + + private async Task RetrieveTestRootAsync(TestMode mode) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _testRunnerApi.RetrieveTestList(mode, root => + { + tcs.TrySetResult(root); + }); + + // Ensure the editor pumps at least one additional update in case the window is unfocused. + EditorApplication.QueuePlayerLoopUpdate(); + + var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(30))).ConfigureAwait(true); + if (completed != tcs.Task) + { + McpLog.Warn($"[TestRunnerService] Timeout waiting for test retrieval callback for {mode}"); + return null; + } + + try + { + return await tcs.Task.ConfigureAwait(true); + } + catch (Exception ex) + { + McpLog.Error($"[TestRunnerService] Error retrieving tests for {mode}: {ex.Message}\n{ex.StackTrace}"); + return null; + } + } + + private static void CollectFromNode( + ITestAdaptor node, + TestMode mode, + List> output, + HashSet seen, + List path) + { + if (node == null) + { + return; + } + + bool hasName = !string.IsNullOrEmpty(node.Name); + if (hasName) + { + path.Add(node.Name); + } + + bool hasChildren = node.HasChildren && node.Children != null; + + if (!hasChildren) + { + string fullName = string.IsNullOrEmpty(node.FullName) ? node.Name ?? string.Empty : node.FullName; + string key = $"{mode}:{fullName}"; + + if (!string.IsNullOrEmpty(fullName) && seen.Add(key)) + { + string computedPath = path.Count > 0 ? string.Join("/", path) : fullName; + output.Add(new Dictionary + { + ["name"] = node.Name ?? fullName, + ["full_name"] = fullName, + ["path"] = computedPath, + ["mode"] = mode.ToString(), + }); + } + } + else if (node.Children != null) + { + foreach (var child in node.Children) + { + CollectFromNode(child, mode, output, seen, path); + } + } + + if (hasName && path.Count > 0) + { + path.RemoveAt(path.Count - 1); + } + } + + #endregion + } + + /// + /// Summary of a Unity test run. + /// + public sealed class TestRunResult + { + internal TestRunResult(TestRunSummary summary, IReadOnlyList results) + { + Summary = summary; + Results = results; + } + + public TestRunSummary Summary { get; } + public IReadOnlyList Results { get; } + + public int Total => Summary.Total; + public int Passed => Summary.Passed; + public int Failed => Summary.Failed; + public int Skipped => Summary.Skipped; + + public object ToSerializable(string mode) + { + return new + { + mode, + summary = Summary.ToSerializable(), + results = Results.Select(r => r.ToSerializable()).ToList(), + }; + } + + internal static TestRunResult Create(ITestResultAdaptor summary, IReadOnlyList tests) + { + var materializedTests = tests.Select(TestRunTestResult.FromAdaptor).ToList(); + + int passed = summary?.PassCount + ?? materializedTests.Count(t => string.Equals(t.State, "Passed", StringComparison.OrdinalIgnoreCase)); + int failed = summary?.FailCount + ?? materializedTests.Count(t => string.Equals(t.State, "Failed", StringComparison.OrdinalIgnoreCase)); + int skipped = summary?.SkipCount + ?? materializedTests.Count(t => string.Equals(t.State, "Skipped", StringComparison.OrdinalIgnoreCase)); + + double duration = summary?.Duration + ?? materializedTests.Sum(t => t.DurationSeconds); + + int total = summary != null ? passed + failed + skipped : materializedTests.Count; + + var summaryPayload = new TestRunSummary( + total, + passed, + failed, + skipped, + duration, + summary?.ResultState ?? "Unknown"); + + return new TestRunResult(summaryPayload, materializedTests); + } + } + + public sealed class TestRunSummary + { + internal TestRunSummary(int total, int passed, int failed, int skipped, double durationSeconds, string resultState) + { + Total = total; + Passed = passed; + Failed = failed; + Skipped = skipped; + DurationSeconds = durationSeconds; + ResultState = resultState; + } + + public int Total { get; } + public int Passed { get; } + public int Failed { get; } + public int Skipped { get; } + public double DurationSeconds { get; } + public string ResultState { get; } + + internal object ToSerializable() + { + return new + { + total = Total, + passed = Passed, + failed = Failed, + skipped = Skipped, + durationSeconds = DurationSeconds, + resultState = ResultState, + }; + } + } + + public sealed class TestRunTestResult + { + internal TestRunTestResult( + string name, + string fullName, + string state, + double durationSeconds, + string message, + string stackTrace, + string output) + { + Name = name; + FullName = fullName; + State = state; + DurationSeconds = durationSeconds; + Message = message; + StackTrace = stackTrace; + Output = output; + } + + public string Name { get; } + public string FullName { get; } + public string State { get; } + public double DurationSeconds { get; } + public string Message { get; } + public string StackTrace { get; } + public string Output { get; } + + internal object ToSerializable() + { + return new + { + name = Name, + fullName = FullName, + state = State, + durationSeconds = DurationSeconds, + message = Message, + stackTrace = StackTrace, + output = Output, + }; + } + + internal static TestRunTestResult FromAdaptor(ITestResultAdaptor adaptor) + { + if (adaptor == null) + { + return new TestRunTestResult(string.Empty, string.Empty, "Unknown", 0.0, string.Empty, string.Empty, string.Empty); + } + + return new TestRunTestResult( + adaptor.Name, + adaptor.FullName, + adaptor.ResultState, + adaptor.Duration, + adaptor.Message, + adaptor.StackTrace, + adaptor.Output); + } + } +} diff --git a/MCPForUnity/Editor/Services/TestRunnerService.cs.meta b/MCPForUnity/Editor/Services/TestRunnerService.cs.meta new file mode 100644 index 00000000..20d0e402 --- /dev/null +++ b/MCPForUnity/Editor/Services/TestRunnerService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 18db1e25b13e14b0b9b186c751e397d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs index ebbbff75..6eba6fda 100644 --- a/MCPForUnity/Editor/Tools/RunTests.cs +++ b/MCPForUnity/Editor/Tools/RunTests.cs @@ -2,8 +2,8 @@ using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Resources.Tests; +using MCPForUnity.Editor.Services; using Newtonsoft.Json.Linq; -using UnityEditor.TestTools.TestRunner.Api; namespace MCPForUnity.Editor.Tools { @@ -42,12 +42,11 @@ public static async Task HandleCommand(JObject @params) // Preserve default timeout if parsing fails } - using var executor = new TestRunExecutor(); - Task runTask; - + var testService = MCPServiceLocator.Tests; + Task runTask; try { - runTask = executor.RunTestsAsync(parsedMode.Value); + runTask = testService.RunTestsAsync(parsedMode.Value); } catch (Exception ex) { From 1c8c6715c1b4716a8867c7df010f338b66987a94 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 00:30:57 -0400 Subject: [PATCH 33/42] feat: add resource retrieval telemetry tracking with new record type and helper function --- MCPForUnity/UnityMcpServer~/src/telemetry.py | 52 +++++++++++++------ .../src/telemetry_decorator.py | 2 +- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry.py b/MCPForUnity/UnityMcpServer~/src/telemetry.py index aa8b8ab4..8af77046 100644 --- a/MCPForUnity/UnityMcpServer~/src/telemetry.py +++ b/MCPForUnity/UnityMcpServer~/src/telemetry.py @@ -20,7 +20,7 @@ import sys import threading import time -from typing import Optional, Dict, Any +from typing import Any from urllib.parse import urlparse import uuid @@ -55,6 +55,7 @@ class RecordType(str, Enum): USAGE = "usage" LATENCY = "latency" FAILURE = "failure" + RESOURCE_RETRIEVAL = "resource_retrieval" TOOL_EXECUTION = "tool_execution" UNITY_CONNECTION = "unity_connection" CLIENT_CONNECTION = "client_connection" @@ -78,8 +79,8 @@ class TelemetryRecord: timestamp: float customer_uuid: str session_id: str - data: Dict[str, Any] - milestone: Optional[MilestoneType] = None + data: dict[str, Any] + milestone: MilestoneType | None = None class TelemetryConfig: @@ -208,8 +209,8 @@ class TelemetryCollector: def __init__(self): self.config = TelemetryConfig() - self._customer_uuid: Optional[str] = None - self._milestones: Dict[str, Dict[str, Any]] = {} + self._customer_uuid: str | None = None + self._milestones: dict[str, dict[str, Any]] = {} self._lock: threading.Lock = threading.Lock() # Bounded queue with single background worker (records only; no context propagation) self._queue: "queue.Queue[TelemetryRecord]" = queue.Queue(maxsize=1000) @@ -262,7 +263,7 @@ def _save_milestones(self): except OSError as e: logger.warning(f"Failed to save milestones: {e}", exc_info=True) - def record_milestone(self, milestone: MilestoneType, data: Optional[Dict[str, Any]] = None) -> bool: + def record_milestone(self, milestone: MilestoneType, data: dict[str, Any] | None = None) -> bool: """Record a milestone event, returns True if this is the first occurrence""" if not self.config.enabled: return False @@ -288,8 +289,8 @@ def record_milestone(self, milestone: MilestoneType, data: Optional[Dict[str, An def record(self, record_type: RecordType, - data: Dict[str, Any], - milestone: Optional[MilestoneType] = None): + data: dict[str, Any], + milestone: MilestoneType | None = None): """Record a telemetry event (async, non-blocking)""" if not self.config.enabled: return @@ -393,7 +394,7 @@ def _send_telemetry(self, record: TelemetryRecord): # Global telemetry instance -_telemetry_collector: Optional[TelemetryCollector] = None +_telemetry_collector: TelemetryCollector | None = None def get_telemetry() -> TelemetryCollector: @@ -405,18 +406,18 @@ def get_telemetry() -> TelemetryCollector: def record_telemetry(record_type: RecordType, - data: Dict[str, Any], - milestone: Optional[MilestoneType] = None): + data: dict[str, Any], + milestone: MilestoneType | None = None): """Convenience function to record telemetry""" get_telemetry().record(record_type, data, milestone) -def record_milestone(milestone: MilestoneType, data: Optional[Dict[str, Any]] = None) -> bool: +def record_milestone(milestone: MilestoneType, data: dict[str, Any] | None = None) -> bool: """Convenience function to record a milestone""" return get_telemetry().record_milestone(milestone, data) -def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: Optional[str] = None, sub_action: Optional[str] = None): +def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: str | None = None, sub_action: str | None = None): """Record tool usage telemetry Args: @@ -445,7 +446,28 @@ def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: record_telemetry(RecordType.TOOL_EXECUTION, data) -def record_latency(operation: str, duration_ms: float, metadata: Optional[Dict[str, Any]] = None): +def record_resource_usage(resource_name: str, success: bool, duration_ms: float, error: str | None = None): + """Record resource usage telemetry + + Args: + resource_name: Name of the resource invoked (e.g., 'get_tests'). + success: Whether the resource completed successfully. + duration_ms: Execution duration in milliseconds. + error: Optional error message (truncated if present). + """ + data = { + "resource_name": resource_name, + "success": success, + "duration_ms": round(duration_ms, 2) + } + + if error: + data["error"] = str(error)[:200] # Limit error message length + + record_telemetry(RecordType.RESOURCE_RETRIEVAL, data) + + +def record_latency(operation: str, duration_ms: float, metadata: dict[str, Any] | None = None): """Record latency telemetry""" data = { "operation": operation, @@ -458,7 +480,7 @@ def record_latency(operation: str, duration_ms: float, metadata: Optional[Dict[s record_telemetry(RecordType.LATENCY, data) -def record_failure(component: str, error: str, metadata: Optional[Dict[str, Any]] = None): +def record_failure(component: str, error: str, metadata: dict[str, Any] | None = None): """Record failure telemetry""" data = { "component": component, diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py b/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py index 32f8a7cf..446ead56 100644 --- a/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py +++ b/MCPForUnity/UnityMcpServer~/src/telemetry_decorator.py @@ -8,7 +8,7 @@ import time from typing import Callable, Any -from telemetry import record_tool_usage, record_milestone, MilestoneType +from telemetry import record_resource_usage, record_tool_usage, record_milestone, MilestoneType _log = logging.getLogger("unity-mcp-telemetry") _decorator_log_count = 0 From 012ea6b7439bd1f2593864d98d03d9d95d7bdd03 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 00:38:34 -0400 Subject: [PATCH 34/42] fix: convert tool functions to async and await ctx.info calls --- .../UnityMcpServer~/src/tools/manage_asset.py | 2 +- .../UnityMcpServer~/src/tools/manage_editor.py | 4 ++-- .../src/tools/manage_gameobject.py | 4 ++-- .../src/tools/manage_menu_item.py | 2 +- .../UnityMcpServer~/src/tools/manage_prefabs.py | 4 ++-- .../UnityMcpServer~/src/tools/manage_scene.py | 4 ++-- .../UnityMcpServer~/src/tools/manage_script.py | 16 ++++++++-------- .../UnityMcpServer~/src/tools/manage_shader.py | 4 ++-- .../UnityMcpServer~/src/tools/read_console.py | 2 +- .../UnityMcpServer~/src/tools/resource_tools.py | 6 +++--- .../src/tools/script_apply_edits.py | 4 ++-- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py index 5e21d2ce..f1041a47 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py @@ -32,7 +32,7 @@ async def manage_asset( page_size: Annotated[int, "Page size for pagination"] | None = None, page_number: Annotated[int, "Page number for pagination"] | None = None ) -> dict[str, Any]: - ctx.info(f"Processing manage_asset: {action}") + await ctx.info(f"Processing manage_asset: {action}") # Ensure properties is a dict if None if properties is None: properties = {} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py index c0de76c2..2994bef8 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py @@ -9,7 +9,7 @@ @mcp_for_unity_tool( description="Controls and queries the Unity editor's state and settings" ) -def manage_editor( +async def manage_editor( ctx: Context, action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows", "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."], @@ -22,7 +22,7 @@ def manage_editor( layer_name: Annotated[str, "Layer name when adding and removing layers"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_editor: {action}") + await ctx.info(f"Processing manage_editor: {action}") try: # Diagnostics: quick telemetry checks if action == "telemetry_status": diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py index a8ca1609..1a9625d0 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py @@ -8,7 +8,7 @@ @mcp_for_unity_tool( description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data." ) -def manage_gameobject( +async def manage_gameobject( ctx: Context, action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."], target: Annotated[str, @@ -64,7 +64,7 @@ def manage_gameobject( includeNonPublicSerialized: Annotated[bool, "Controls whether serialization of private [SerializeField] fields is included"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_gameobject: {action}") + await ctx.info(f"Processing manage_gameobject: {action}") try: # Validate parameter usage to prevent silent failures if action == "find": diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py index 5463614d..20fa1d38 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py @@ -22,7 +22,7 @@ async def manage_menu_item( refresh: Annotated[bool, "Optional flag to force refresh of the menu cache when listing"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_menu_item: {action}") + await ctx.info(f"Processing manage_menu_item: {action}") # Prepare parameters for the C# handler params_dict: dict[str, Any] = { "action": action, diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py index ea89201c..1ad65451 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py @@ -8,7 +8,7 @@ @mcp_for_unity_tool( description="Bridge for prefab management commands (stage control and creation)." ) -def manage_prefabs( +async def manage_prefabs( ctx: Context, action: Annotated[Literal[ "open_stage", @@ -29,7 +29,7 @@ def manage_prefabs( search_inactive: Annotated[bool, "Include inactive objects when resolving the target name"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_prefabs: {action}") + await ctx.info(f"Processing manage_prefabs: {action}") try: params: dict[str, Any] = {"action": action} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py index 09494e4a..fbbcf3ca 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py @@ -6,7 +6,7 @@ @mcp_for_unity_tool(description="Manage Unity scenes") -def manage_scene( +async def manage_scene( ctx: Context, action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."], name: Annotated[str, @@ -16,7 +16,7 @@ def manage_scene( build_index: Annotated[int, "Build index for load/build settings actions"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_scene: {action}") + await ctx.info(f"Processing manage_scene: {action}") try: # Coerce numeric inputs defensively def _coerce_int(value, default=None): diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py index cad6a88c..e8b38775 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py @@ -75,7 +75,7 @@ def _split_uri(uri: str) -> tuple[str, str]: - Lines, columns are 1-indexed - Tabs count as 1 column""" )) -def apply_text_edits( +async def apply_text_edits( ctx: Context, uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script, i.e. a list of {startLine,startCol,endLine,endCol,newText} (1-indexed!)"], @@ -86,7 +86,7 @@ def apply_text_edits( options: Annotated[dict[str, Any], "Optional options, used to pass additional options to the script editor"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing apply_text_edits: {uri}") + await ctx.info(f"Processing apply_text_edits: {uri}") name, directory = _split_uri(uri) # Normalize common aliases/misuses for resilience: @@ -360,7 +360,7 @@ def create_script( script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None, namespace: Annotated[str, "Namespace for the script"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing create_script: {path}") + await ctx.info(f"Processing create_script: {path}") name = os.path.splitext(os.path.basename(path))[0] directory = os.path.dirname(path) # Local validation to avoid round-trips on obviously bad input @@ -396,7 +396,7 @@ def delete_script( uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] ) -> dict[str, Any]: """Delete a C# script by URI.""" - ctx.info(f"Processing delete_script: {uri}") + await ctx.info(f"Processing delete_script: {uri}") name, directory = _split_uri(uri) if not directory or directory.split("/")[0].lower() != "assets": return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} @@ -414,7 +414,7 @@ def validate_script( include_diagnostics: Annotated[bool, "Include full diagnostics and summary"] = False ) -> dict[str, Any]: - ctx.info(f"Processing validate_script: {uri}") + await ctx.info(f"Processing validate_script: {uri}") name, directory = _split_uri(uri) if not directory or directory.split("/")[0].lower() != "assets": return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} @@ -451,7 +451,7 @@ def manage_script( "Type hint (e.g., 'MonoBehaviour')"] | None = None, namespace: Annotated[str, "Namespace for the script"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_script: {action}") + await ctx.info(f"Processing manage_script: {action}") try: # Prepare parameters for Unity params = { @@ -509,7 +509,7 @@ def manage_script( - guards: header/using guard enabled flag""" )) def manage_script_capabilities(ctx: Context) -> dict[str, Any]: - ctx.info("Processing manage_script_capabilities") + await ctx.info("Processing manage_script_capabilities") try: # Keep in sync with server/Editor ManageScript implementation ops = [ @@ -537,7 +537,7 @@ def get_sha( ctx: Context, uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] ) -> dict[str, Any]: - ctx.info(f"Processing get_sha: {uri}") + await ctx.info(f"Processing get_sha: {uri}") try: name, directory = _split_uri(uri) params = {"action": "get_sha", "name": name, "path": directory} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py index 9c199661..e44611cd 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py @@ -9,7 +9,7 @@ @mcp_for_unity_tool( description="Manages shader scripts in Unity (create, read, update, delete)." ) -def manage_shader( +async def manage_shader( ctx: Context, action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."], name: Annotated[str, "Shader name (no .cs extension)"], @@ -17,7 +17,7 @@ def manage_shader( contents: Annotated[str, "Shader code for 'create'/'update'"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing manage_shader: {action}") + await ctx.info(f"Processing manage_shader: {action}") try: # Prepare parameters for Unity params = { diff --git a/MCPForUnity/UnityMcpServer~/src/tools/read_console.py b/MCPForUnity/UnityMcpServer~/src/tools/read_console.py index 5fc9a096..4ff1e837 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/read_console.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/read_console.py @@ -25,7 +25,7 @@ def read_console( include_stacktrace: Annotated[bool, "Include stack traces in output"] | None = None ) -> dict[str, Any]: - ctx.info(f"Processing read_console: {action}") + await ctx.info(f"Processing read_console: {action}") # Set defaults if values are None action = action if action is not None else 'get' types = types if types is not None else ['error', 'warning', 'log'] diff --git a/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py b/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py index f28fc589..ab4aaf07 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py @@ -142,7 +142,7 @@ async def list_resources( limit: Annotated[int, "Page limit"] = 200, project_root: Annotated[str, "Project path"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing list_resources: {pattern}") + await ctx.info(f"Processing list_resources: {pattern}") try: project = _resolve_project_root(project_root) base = (project / under).resolve() @@ -202,7 +202,7 @@ async def read_resource( "The project root directory"] | None = None, request: Annotated[str, "The request ID"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing read_resource: {uri}") + await ctx.info(f"Processing read_resource: {uri}") try: # Serve the canonical spec directly when requested (allow bare or with scheme) if uri in ("unity://spec/script-edits", "spec/script-edits", "script-edits"): @@ -357,7 +357,7 @@ async def find_in_file( max_results: Annotated[int, "Cap results to avoid huge payloads"] = 200, ) -> dict[str, Any]: - ctx.info(f"Processing find_in_file: {uri}") + await ctx.info(f"Processing find_in_file: {uri}") try: project = _resolve_project_root(project_root) p = _resolve_safe_path_from_uri(uri, project) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py b/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py index 59fbbc61..30980b1f 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py @@ -354,7 +354,7 @@ def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rew } ]""" )) -def script_apply_edits( +async def script_apply_edits( ctx: Context, name: Annotated[str, "Name of the script to edit"], path: Annotated[str, "Path to the script to edit under Assets/ directory"], @@ -366,7 +366,7 @@ def script_apply_edits( namespace: Annotated[str, "Namespace of the script to edit"] | None = None, ) -> dict[str, Any]: - ctx.info(f"Processing script_apply_edits: {name}") + await ctx.info(f"Processing script_apply_edits: {name}") # Normalize locator first so downstream calls target the correct script file. name, path = _normalize_script_locator(name, path) # Normalize unsupported or aliased ops to known structured/text paths From 403713dcce71033fa53e1aa77ee511862acd868c Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 01:55:00 -0400 Subject: [PATCH 35/42] refactor: reorganize menu item functionality into separate execute and get commands An MCP resource for retrieval, and a simple command to execute. Because it's a resource, it's easier for the user to see what's in the menu items --- MCPForUnity/Editor/MCPForUnityBridge.cs | 1 - .../{Tools => Resources}/MenuItems.meta | 2 +- .../Resources/MenuItems/GetMenuItems.cs | 71 ++++++++++++++ .../Resources/MenuItems/GetMenuItems.cs.meta | 2 +- ...MenuItemExecutor.cs => ExecuteMenuItem.cs} | 15 ++- ...ecutor.cs.meta => ExecuteMenuItem.cs.meta} | 2 +- .../Editor/Tools/MenuItems/ManageMenuItem.cs | 42 -------- .../Tools/MenuItems/ManageMenuItem.cs.meta | 11 --- .../Editor/Tools/MenuItems/MenuItemsReader.cs | 95 ------------------- .../Tools/MenuItems/MenuItemsReader.cs.meta | 11 --- .../src/resources/menu_items.py | 25 +++++ MCPForUnity/UnityMcpServer~/src/server.py | 6 +- .../src/tools/execute_menu_item.py | 25 +++++ .../src/tools/manage_menu_item.py | 41 -------- .../{Tools/MenuItems.meta => Resources.meta} | 2 +- .../GetMenuItemsTests.cs} | 39 ++------ .../GetMenuItemsTests.cs.meta} | 0 .../EditMode/Tools/CommandRegistryTests.cs | 2 +- ...ecutorTests.cs => ExecuteMenuItemTests.cs} | 12 +-- ...s.cs.meta => ExecuteMenuItemTests.cs.meta} | 0 .../Tools/MenuItems/ManageMenuItemTests.cs | 47 --------- 21 files changed, 151 insertions(+), 300 deletions(-) rename MCPForUnity/Editor/{Tools => Resources}/MenuItems.meta (77%) create mode 100644 MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta => MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta (83%) rename MCPForUnity/Editor/Tools/{MenuItems/MenuItemExecutor.cs => ExecuteMenuItem.cs} (84%) rename MCPForUnity/Editor/Tools/{MenuItems/MenuItemExecutor.cs.meta => ExecuteMenuItem.cs.meta} (83%) delete mode 100644 MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs delete mode 100644 MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta delete mode 100644 MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs delete mode 100644 MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta create mode 100644 MCPForUnity/UnityMcpServer~/src/resources/menu_items.py create mode 100644 MCPForUnity/UnityMcpServer~/src/tools/execute_menu_item.py delete mode 100644 MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{Tools/MenuItems.meta => Resources.meta} (77%) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{Tools/MenuItems/MenuItemsReaderTests.cs => Resources/GetMenuItemsTests.cs} (62%) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{Tools/MenuItems/MenuItemsReaderTests.cs.meta => Resources/GetMenuItemsTests.cs.meta} (100%) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/{MenuItems/MenuItemExecutorTests.cs => ExecuteMenuItemTests.cs} (75%) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/{MenuItems/MenuItemExecutorTests.cs.meta => ExecuteMenuItemTests.cs.meta} (100%) delete mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs b/MCPForUnity/Editor/MCPForUnityBridge.cs index 6c87e662..5fb9f694 100644 --- a/MCPForUnity/Editor/MCPForUnityBridge.cs +++ b/MCPForUnity/Editor/MCPForUnityBridge.cs @@ -14,7 +14,6 @@ using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using MCPForUnity.Editor.Tools; -using MCPForUnity.Editor.Tools.MenuItems; using MCPForUnity.Editor.Tools.Prefabs; namespace MCPForUnity.Editor diff --git a/MCPForUnity/Editor/Tools/MenuItems.meta b/MCPForUnity/Editor/Resources/MenuItems.meta similarity index 77% rename from MCPForUnity/Editor/Tools/MenuItems.meta rename to MCPForUnity/Editor/Resources/MenuItems.meta index ffbda8e7..df20ed6c 100644 --- a/MCPForUnity/Editor/Tools/MenuItems.meta +++ b/MCPForUnity/Editor/Resources/MenuItems.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2df8f144c6e684ec3bfd53e4a48f06ee +guid: bca79cd3ef8ed466f9e50e2dc7850e46 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs b/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs new file mode 100644 index 00000000..c554be2d --- /dev/null +++ b/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; + +namespace MCPForUnity.Editor.Resources.MenuItems +{ + /// + /// Provides a simple read-only resource that returns Unity menu items. + /// + [McpForUnityResource("get_menu_items")] + public static class GetMenuItems + { + private static List _cached; + + [InitializeOnLoadMethod] + private static void BuildCache() => Refresh(); + + public static object HandleCommand(JObject @params) + { + bool forceRefresh = @params?["refresh"]?.ToObject() ?? false; + string search = @params?["search"]?.ToString(); + + var items = GetMenuItemsInternal(forceRefresh); + + if (!string.IsNullOrEmpty(search)) + { + items = items + .Where(item => item.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) + .ToList(); + } + + string message = $"Retrieved {items.Count} menu items"; + return Response.Success(message, items); + } + + internal static List GetMenuItemsInternal(bool forceRefresh) + { + if (forceRefresh || _cached == null) + { + Refresh(); + } + + return (_cached ?? new List()).ToList(); + } + + private static void Refresh() + { + try + { + var methods = TypeCache.GetMethodsWithAttribute(); + _cached = methods + .SelectMany(m => m + .GetCustomAttributes(typeof(MenuItem), false) + .OfType() + .Select(attr => attr.menuItem)) + .Where(s => !string.IsNullOrEmpty(s)) + .Distinct(StringComparer.Ordinal) + .OrderBy(s => s, StringComparer.Ordinal) + .ToList(); + } + catch (Exception ex) + { + McpLog.Error($"[GetMenuItems] Failed to scan menu items: {ex}"); + _cached ??= new List(); + } + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta b/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta similarity index 83% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta rename to MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta index 6f1a8c2b..fde7829b 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs.meta +++ b/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2b36e5f577aa1481c8758831c49d8f9d +guid: 04eeea61eb5c24033a88013845d25f23 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs similarity index 84% rename from MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs rename to MCPForUnity/Editor/Tools/ExecuteMenuItem.cs index 193a80f6..9e3ac320 100644 --- a/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs +++ b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; +using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; using UnityEditor; -using MCPForUnity.Editor.Helpers; -namespace MCPForUnity.Editor.Tools.MenuItems +namespace MCPForUnity.Editor.Tools { - /// - /// Executes Unity Editor menu items by path with safety checks. - /// - public static class MenuItemExecutor + [McpForUnityTool("execute_menu_item")] + public static class ExecuteMenuItem { // Basic blacklist to prevent execution of disruptive menu items. private static readonly HashSet _menuPathBlacklist = new HashSet( @@ -19,10 +17,11 @@ public static class MenuItemExecutor }; /// - /// Execute a specific menu item. Expects 'menu_path' or 'menuPath' in params. + /// Routes actions: execute, list, exists, refresh /// - public static object Execute(JObject @params) + public static object HandleCommand(JObject @params) { + McpLog.Info("[ExecuteMenuItem] Handling menu item command"); string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString(); if (string.IsNullOrWhiteSpace(menuPath)) { diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta similarity index 83% rename from MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta rename to MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta index 2e9f4223..1caedb7f 100644 --- a/MCPForUnity/Editor/Tools/MenuItems/MenuItemExecutor.cs.meta +++ b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1ccc7c6ff549542e1ae4ba3463ae79d2 +guid: 269232350d16a464091aea9e9fcc9b55 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs b/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs deleted file mode 100644 index e4b7eaf7..00000000 --- a/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools.MenuItems -{ - [McpForUnityTool("manage_menu_item")] - public static class ManageMenuItem - { - /// - /// Routes actions: execute, list, exists, refresh - /// - public static object HandleCommand(JObject @params) - { - string action = @params["action"]?.ToString()?.ToLowerInvariant(); - if (string.IsNullOrEmpty(action)) - { - return Response.Error("Action parameter is required. Valid actions are: execute, list, exists, refresh."); - } - - try - { - switch (action) - { - case "execute": - return MenuItemExecutor.Execute(@params); - case "list": - return MenuItemsReader.List(@params); - case "exists": - return MenuItemsReader.Exists(@params); - default: - return Response.Error($"Unknown action: '{action}'. Valid actions are: execute, list, exists, refresh."); - } - } - catch (Exception e) - { - McpLog.Error($"[ManageMenuItem] Action '{action}' failed: {e}"); - return Response.Error($"Internal error: {e.Message}"); - } - } - } -} diff --git a/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta b/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta deleted file mode 100644 index aba1f496..00000000 --- a/MCPForUnity/Editor/Tools/MenuItems/ManageMenuItem.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 77808278b21a6474a90f3abb91483f71 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs b/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs deleted file mode 100644 index 60c94125..00000000 --- a/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using UnityEditor; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools.MenuItems -{ - /// - /// Provides read/list/exists capabilities for Unity menu items with caching. - /// - public static class MenuItemsReader - { - private static List _cached; - - [InitializeOnLoadMethod] - private static void Build() => Refresh(); - - /// - /// Returns the cached list, refreshing if necessary. - /// - public static IReadOnlyList AllMenuItems() => _cached ??= Refresh(); - - /// - /// Rebuilds the cached list from reflection. - /// - private static List Refresh() - { - try - { - var methods = TypeCache.GetMethodsWithAttribute(); - _cached = methods - // Methods can have multiple [MenuItem] attributes; collect them all - .SelectMany(m => m - .GetCustomAttributes(typeof(MenuItem), false) - .OfType() - .Select(attr => attr.menuItem)) - .Where(s => !string.IsNullOrEmpty(s)) - .Distinct(StringComparer.Ordinal) // Ensure no duplicates - .OrderBy(s => s, StringComparer.Ordinal) // Ensure consistent ordering - .ToList(); - return _cached; - } - catch (Exception e) - { - McpLog.Error($"[MenuItemsReader] Failed to scan menu items: {e}"); - _cached = _cached ?? new List(); - return _cached; - } - } - - /// - /// Returns a list of menu items. Optional 'search' param filters results. - /// - public static object List(JObject @params) - { - string search = @params["search"]?.ToString(); - bool doRefresh = @params["refresh"]?.ToObject() ?? false; - if (doRefresh || _cached == null) - { - Refresh(); - } - - IEnumerable result = _cached ?? Enumerable.Empty(); - if (!string.IsNullOrEmpty(search)) - { - result = result.Where(s => s.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0); - } - - return Response.Success("Menu items retrieved.", result.ToList()); - } - - /// - /// Checks if a given menu path exists in the cache. - /// - public static object Exists(JObject @params) - { - string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString(); - if (string.IsNullOrWhiteSpace(menuPath)) - { - return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty."); - } - - bool doRefresh = @params["refresh"]?.ToObject() ?? false; - if (doRefresh || _cached == null) - { - Refresh(); - } - - bool exists = (_cached ?? new List()).Contains(menuPath); - return Response.Success($"Exists check completed for '{menuPath}'.", new { exists }); - } - } -} diff --git a/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta b/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta deleted file mode 100644 index 78fd7ab4..00000000 --- a/MCPForUnity/Editor/Tools/MenuItems/MenuItemsReader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 37f212f83e8854ed7b5454d3733e4bfa -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py b/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py new file mode 100644 index 00000000..296fb900 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py @@ -0,0 +1,25 @@ +from models import MCPResponse +from registry import mcp_for_unity_resource +from unity_connection import async_send_command_with_retry + + +class GetMenuItemsResponse(MCPResponse): + data: list[str] = [] + + +@mcp_for_unity_resource( + uri="mcpforunity://menu-items", + name="get_menu_items", + description="Provides a list of all menu items." +) +async def get_menu_items() -> GetMenuItemsResponse: + """Provides a list of all menu items.""" + # Later versions of FastMCP support these as query parameters + # See: https://gofastmcp.com/servers/resources#query-parameters + params = { + "refresh": False, + "search": "", + } + + response = await async_send_command_with_retry("get_menu_items", params) + return GetMenuItemsResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/server.py b/MCPForUnity/UnityMcpServer~/src/server.py index acab160b..e4442eec 100644 --- a/MCPForUnity/UnityMcpServer~/src/server.py +++ b/MCPForUnity/UnityMcpServer~/src/server.py @@ -173,7 +173,7 @@ def asset_creation_strategy() -> str: return ( "Available MCP for Unity Server Tools:\n\n" "- `manage_editor`: Controls editor state and queries info.\n" - "- `manage_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n" + "- `execute_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n" "- `read_console`: Reads or clears Unity console messages, with filtering options.\n" "- `manage_scene`: Manages scenes.\n" "- `manage_gameobject`: Manages GameObjects in the scene.\n" @@ -185,9 +185,7 @@ def asset_creation_strategy() -> str: "- Always include a camera and main light in your scenes.\n" "- Unless specified otherwise, paths are relative to the project's `Assets/` folder.\n" "- After creating or modifying scripts with `manage_script`, allow Unity to recompile; use `read_console` to check for compile errors.\n" - "- Use `manage_menu_item` for interacting with Unity systems and third party tools like a user would.\n" - "- List menu items before using them if you are unsure of the menu path.\n" - "- If a menu item seems missing, refresh the cache: use manage_menu_item with action='list' and refresh=true, or action='refresh'. Avoid refreshing every time; prefer refresh only when the menu set likely changed.\n" + "- Use `execute_menu_item` for interacting with Unity systems and third party tools like a user would.\n" ) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/execute_menu_item.py b/MCPForUnity/UnityMcpServer~/src/tools/execute_menu_item.py new file mode 100644 index 00000000..03d419a0 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tools/execute_menu_item.py @@ -0,0 +1,25 @@ +""" +Defines the execute_menu_item tool for executing and reading Unity Editor menu items. +""" +from typing import Annotated, Any + +from mcp.server.fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_tool +from unity_connection import async_send_command_with_retry + + +@mcp_for_unity_tool( + description="Execute a Unity menu item by path." +) +async def execute_menu_item( + ctx: Context, + menu_path: Annotated[str, + "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None, +) -> MCPResponse: + await ctx.info(f"Processing execute_menu_item: {menu_path}") + params_dict: dict[str, Any] = {"menuPath": menu_path} + params_dict = {k: v for k, v in params_dict.items() if v is not None} + result = await async_send_command_with_retry("execute_menu_item", params_dict) + return MCPResponse(**result) if isinstance(result, dict) else result diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py deleted file mode 100644 index 20fa1d38..00000000 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_menu_item.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Defines the manage_menu_item tool for executing and reading Unity Editor menu items. -""" -import asyncio -from typing import Annotated, Any, Literal - -from mcp.server.fastmcp import Context -from registry import mcp_for_unity_tool -from unity_connection import async_send_command_with_retry - - -@mcp_for_unity_tool( - description="Manage Unity menu items (execute/list/exists). If you're not sure what menu item to use, use the 'list' action to find it before using 'execute'." -) -async def manage_menu_item( - ctx: Context, - action: Annotated[Literal["execute", "list", "exists"], "Read and execute Unity menu items."], - menu_path: Annotated[str, - "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None, - search: Annotated[str, - "Optional filter string for 'list' (e.g., 'Save')"] | None = None, - refresh: Annotated[bool, - "Optional flag to force refresh of the menu cache when listing"] | None = None, -) -> dict[str, Any]: - await ctx.info(f"Processing manage_menu_item: {action}") - # Prepare parameters for the C# handler - params_dict: dict[str, Any] = { - "action": action, - "menuPath": menu_path, - "search": search, - "refresh": refresh, - } - # Remove None values - params_dict = {k: v for k, v in params_dict.items() if v is not None} - - # Get the current asyncio event loop - loop = asyncio.get_running_loop() - - # Use centralized async retry helper - result = await async_send_command_with_retry("manage_menu_item", params_dict, loop=loop) - return result if isinstance(result, dict) else {"success": False, "message": str(result)} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta similarity index 77% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems.meta rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta index fd11c223..efe48a22 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems.meta +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c01321ff6339b4763807adb979c5c427 +guid: 552680d3640c64564b19677d789515c3 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources/GetMenuItemsTests.cs similarity index 62% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources/GetMenuItemsTests.cs index e13e1b90..0aa80729 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources/GetMenuItemsTests.cs @@ -1,19 +1,19 @@ using NUnit.Framework; using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Tools.MenuItems; +using MCPForUnity.Editor.Resources.MenuItems; using System; using System.Linq; -namespace MCPForUnityTests.Editor.Tools.MenuItems +namespace MCPForUnityTests.Editor.Resources.MenuItems { - public class MenuItemsReaderTests + public class GetMenuItemsTests { private static JObject ToJO(object o) => JObject.FromObject(o); [Test] - public void List_NoSearch_ReturnsSuccessAndArray() + public void NoSearch_ReturnsSuccessAndArray() { - var res = MenuItemsReader.List(new JObject()); + var res = GetMenuItems.HandleCommand(new JObject { ["search"] = "", ["refresh"] = false }); var jo = ToJO(res); Assert.IsTrue((bool)jo["success"], "Expected success true"); Assert.IsNotNull(jo["data"], "Expected data field present"); @@ -30,9 +30,9 @@ public void List_NoSearch_ReturnsSuccessAndArray() } [Test] - public void List_SearchNoMatch_ReturnsEmpty() + public void SearchNoMatch_ReturnsEmpty() { - var res = MenuItemsReader.List(new JObject { ["search"] = "___unlikely___term___" }); + var res = GetMenuItems.HandleCommand(new JObject { ["search"] = "___unlikely___term___" }); var jo = ToJO(res); Assert.IsTrue((bool)jo["success"], "Expected success true"); Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array"); @@ -40,10 +40,10 @@ public void List_SearchNoMatch_ReturnsEmpty() } [Test] - public void List_SearchMatchesExistingItem_ReturnsContainingItem() + public void SearchMatchesExistingItem_ReturnsContainingItem() { // Get the full list first - var listRes = MenuItemsReader.List(new JObject()); + var listRes = GetMenuItems.HandleCommand(new JObject { ["search"] = "", ["refresh"] = false }); var listJo = ToJO(listRes); if (listJo["data"] is JArray arr && arr.Count > 0) { @@ -52,7 +52,7 @@ public void List_SearchMatchesExistingItem_ReturnsContainingItem() var term = first.Length > 4 ? first.Substring(1, Math.Min(3, first.Length - 2)) : first; term = term.ToLowerInvariant(); - var res = MenuItemsReader.List(new JObject { ["search"] = term }); + var res = GetMenuItems.HandleCommand(new JObject { ["search"] = term, ["refresh"] = false }); var jo = ToJO(res); Assert.IsTrue((bool)jo["success"], "Expected success true"); Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array"); @@ -65,24 +65,5 @@ public void List_SearchMatchesExistingItem_ReturnsContainingItem() Assert.Pass("No menu items available to perform a content-based search assertion."); } } - - [Test] - public void Exists_MissingParam_ReturnsError() - { - var res = MenuItemsReader.Exists(new JObject()); - var jo = ToJO(res); - Assert.IsFalse((bool)jo["success"], "Expected success false"); - StringAssert.Contains("Required parameter", (string)jo["error"]); - } - - [Test] - public void Exists_Bogus_ReturnsFalse() - { - var res = MenuItemsReader.Exists(new JObject { ["menuPath"] = "Nonexistent/Menu/___unlikely___" }); - var jo = ToJO(res); - Assert.IsTrue((bool)jo["success"], "Expected success true"); - Assert.IsNotNull(jo["data"], "Expected data field present"); - Assert.IsFalse((bool)jo["data"]["exists"], "Expected exists false for bogus menu path"); - } } } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources/GetMenuItemsTests.cs.meta similarity index 100% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemsReaderTests.cs.meta rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources/GetMenuItemsTests.cs.meta diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs index ab204ef0..ed8ef3c6 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs @@ -41,7 +41,7 @@ public void AutoDiscovery_RegistersAllBuiltInTools() "manage_script", "manage_shader", "read_console", - "manage_menu_item", + "execute_menu_item", "manage_prefabs" }; diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteMenuItemTests.cs similarity index 75% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteMenuItemTests.cs index 495f429d..51cbd4cf 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteMenuItemTests.cs @@ -1,17 +1,17 @@ using NUnit.Framework; using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Tools.MenuItems; +using MCPForUnity.Editor.Tools; -namespace MCPForUnityTests.Editor.Tools.MenuItems +namespace MCPForUnityTests.Editor.Tools { - public class MenuItemExecutorTests + public class ExecuteMenuItemTests { private static JObject ToJO(object o) => JObject.FromObject(o); [Test] public void Execute_MissingParam_ReturnsError() { - var res = MenuItemExecutor.Execute(new JObject()); + var res = ExecuteMenuItem.HandleCommand(new JObject()); var jo = ToJO(res); Assert.IsFalse((bool)jo["success"], "Expected success false"); StringAssert.Contains("Required parameter", (string)jo["error"]); @@ -20,7 +20,7 @@ public void Execute_MissingParam_ReturnsError() [Test] public void Execute_Blacklisted_ReturnsError() { - var res = MenuItemExecutor.Execute(new JObject { ["menuPath"] = "File/Quit" }); + var res = ExecuteMenuItem.HandleCommand(new JObject { ["menuPath"] = "File/Quit" }); var jo = ToJO(res); Assert.IsFalse((bool)jo["success"], "Expected success false for blacklisted menu"); StringAssert.Contains("blocked for safety", (string)jo["error"], "Expected blacklist message"); @@ -30,7 +30,7 @@ public void Execute_Blacklisted_ReturnsError() public void Execute_NonBlacklisted_ReturnsImmediateSuccess() { // We don't rely on the menu actually existing; execution is delayed and we only check the immediate response shape - var res = MenuItemExecutor.Execute(new JObject { ["menuPath"] = "File/Save Project" }); + var res = ExecuteMenuItem.HandleCommand(new JObject { ["menuPath"] = "File/Save Project" }); var jo = ToJO(res); Assert.IsTrue((bool)jo["success"], "Expected immediate success response"); StringAssert.Contains("Attempted to execute menu item", (string)jo["message"], "Expected attempt message"); diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteMenuItemTests.cs.meta similarity index 100% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/MenuItemExecutorTests.cs.meta rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteMenuItemTests.cs.meta diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs deleted file mode 100644 index d4188040..00000000 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MenuItems/ManageMenuItemTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using NUnit.Framework; -using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Tools.MenuItems; - -namespace MCPForUnityTests.Editor.Tools.MenuItems -{ - public class ManageMenuItemTests - { - private static JObject ToJO(object o) => JObject.FromObject(o); - - [Test] - public void HandleCommand_UnknownAction_ReturnsError() - { - var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "unknown_action" }); - var jo = ToJO(res); - Assert.IsFalse((bool)jo["success"], "Expected success false for unknown action"); - StringAssert.Contains("Unknown action", (string)jo["error"]); - } - - [Test] - public void HandleCommand_List_RoutesAndReturnsArray() - { - var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "list" }); - var jo = ToJO(res); - Assert.IsTrue((bool)jo["success"], "Expected success true"); - Assert.AreEqual(JTokenType.Array, jo["data"].Type, "Expected data to be an array"); - } - - [Test] - public void HandleCommand_Execute_Blacklisted_RoutesAndErrors() - { - var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "execute", ["menuPath"] = "File/Quit" }); - var jo = ToJO(res); - Assert.IsFalse((bool)jo["success"], "Expected success false"); - StringAssert.Contains("blocked for safety", (string)jo["error"], "Expected blacklist message"); - } - - [Test] - public void HandleCommand_Exists_MissingParam_ReturnsError() - { - var res = ManageMenuItem.HandleCommand(new JObject { ["action"] = "exists" }); - var jo = ToJO(res); - Assert.IsFalse((bool)jo["success"], "Expected success false when missing menuPath"); - StringAssert.Contains("Required parameter", (string)jo["error"]); - } - } -} From 942d2497526793ebbba93b08e169a67998a73e77 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 01:55:25 -0400 Subject: [PATCH 36/42] refactor: rename manage_menu_item to execute_menu_item and update tool examples to use async/await We'll eventually put a section for resources --- README-zh.md | 2 +- README.md | 2 +- docs/CUSTOM_TOOLS.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README-zh.md b/README-zh.md index fee06629..a0b3c60d 100644 --- a/README-zh.md +++ b/README-zh.md @@ -45,7 +45,7 @@ MCP for Unity 作为桥梁,允许 AI 助手(如 Claude、Cursor)通过本 * `manage_asset`: 执行资源操作(导入、创建、修改、删除等)。 * `manage_shader`: 执行着色器 CRUD 操作(创建、读取、修改、删除)。 * `manage_gameobject`: 管理游戏对象:创建、修改、删除、查找和组件操作。 - * `manage_menu_item`: 列出 Unity 编辑器菜单项;检查其存在性或执行它们(例如,执行"File/Save Project")。 + * `execute_menu_item`: 执行 Unity 编辑器菜单项(例如,执行"File/Save Project")。 * `apply_text_edits`: 具有前置条件哈希和原子多编辑批次的精确文本编辑。 * `script_apply_edits`: 结构化 C# 方法/类编辑(插入/替换/删除),具有更安全的边界。 * `validate_script`: 快速验证(基本/标准)以在写入前后捕获语法/结构问题。 diff --git a/README.md b/README.md index 0080a728..fa5aa287 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to * `manage_asset`: Performs asset operations (import, create, modify, delete, etc.). * `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). * `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations. - * `manage_menu_item`: List Unity Editor menu items; and check for their existence or execute them (e.g., execute "File/Save Project"). + * `execute_menu_item`: Executes Unity Editor menu items (e.g., "File/Save Project"). * `apply_text_edits`: Precise text edits with precondition hashes and atomic multi-edit batches. * `script_apply_edits`: Structured C# method/class edits (insert/replace/delete) with safer boundaries. * `validate_script`: Fast validation (basic/standard) to catch syntax/structure issues before/after writes. diff --git a/docs/CUSTOM_TOOLS.md b/docs/CUSTOM_TOOLS.md index d0397295..a212eb09 100644 --- a/docs/CUSTOM_TOOLS.md +++ b/docs/CUSTOM_TOOLS.md @@ -24,12 +24,12 @@ from unity_connection import send_command_with_retry @mcp_for_unity_tool( description="My custom tool that does something amazing" ) -def my_custom_tool( +async def my_custom_tool( ctx: Context, param1: Annotated[str, "Description of param1"], param2: Annotated[int, "Description of param2"] | None = None ) -> dict[str, Any]: - ctx.info(f"Processing my_custom_tool: {param1}") + await ctx.info(f"Processing my_custom_tool: {param1}") # Prepare parameters for Unity params = { @@ -151,11 +151,11 @@ from unity_connection import send_command_with_retry @mcp_for_unity_tool( description="Capture screenshots in Unity, saving them as PNGs" ) -def capture_screenshot( +async def capture_screenshot( ctx: Context, filename: Annotated[str, "Screenshot filename without extension, e.g., screenshot_01"], ) -> dict[str, Any]: - ctx.info(f"Capturing screenshot: {filename}") + await ctx.info(f"Capturing screenshot: {filename}") params = { "action": "capture", From 57d8044631890e393a496bd2aae3d67a356f5141 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 09:20:10 -0400 Subject: [PATCH 37/42] Revert "fix: convert tool functions to async and await ctx.info calls" This reverts commit 012ea6b7439bd1f2593864d98d03d9d95d7bdd03. --- .../UnityMcpServer~/src/tools/manage_asset.py | 2 +- .../UnityMcpServer~/src/tools/manage_editor.py | 4 ++-- .../src/tools/manage_gameobject.py | 4 ++-- .../UnityMcpServer~/src/tools/manage_prefabs.py | 4 ++-- .../UnityMcpServer~/src/tools/manage_scene.py | 4 ++-- .../UnityMcpServer~/src/tools/manage_script.py | 16 ++++++++-------- .../UnityMcpServer~/src/tools/manage_shader.py | 4 ++-- .../UnityMcpServer~/src/tools/read_console.py | 2 +- .../UnityMcpServer~/src/tools/resource_tools.py | 6 +++--- .../src/tools/script_apply_edits.py | 4 ++-- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py index f1041a47..5e21d2ce 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_asset.py @@ -32,7 +32,7 @@ async def manage_asset( page_size: Annotated[int, "Page size for pagination"] | None = None, page_number: Annotated[int, "Page number for pagination"] | None = None ) -> dict[str, Any]: - await ctx.info(f"Processing manage_asset: {action}") + ctx.info(f"Processing manage_asset: {action}") # Ensure properties is a dict if None if properties is None: properties = {} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py index 2994bef8..c0de76c2 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_editor.py @@ -9,7 +9,7 @@ @mcp_for_unity_tool( description="Controls and queries the Unity editor's state and settings" ) -async def manage_editor( +def manage_editor( ctx: Context, action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "get_state", "get_project_root", "get_windows", "get_active_tool", "get_selection", "get_prefab_stage", "set_active_tool", "add_tag", "remove_tag", "get_tags", "add_layer", "remove_layer", "get_layers"], "Get and update the Unity Editor state."], @@ -22,7 +22,7 @@ async def manage_editor( layer_name: Annotated[str, "Layer name when adding and removing layers"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing manage_editor: {action}") + ctx.info(f"Processing manage_editor: {action}") try: # Diagnostics: quick telemetry checks if action == "telemetry_status": diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py index 1a9625d0..a8ca1609 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_gameobject.py @@ -8,7 +8,7 @@ @mcp_for_unity_tool( description="Manage GameObjects. Note: for 'get_components', the `data` field contains a dictionary of component names and their serialized properties. For 'get_component', specify 'component_name' to retrieve only that component's serialized data." ) -async def manage_gameobject( +def manage_gameobject( ctx: Context, action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."], target: Annotated[str, @@ -64,7 +64,7 @@ async def manage_gameobject( includeNonPublicSerialized: Annotated[bool, "Controls whether serialization of private [SerializeField] fields is included"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing manage_gameobject: {action}") + ctx.info(f"Processing manage_gameobject: {action}") try: # Validate parameter usage to prevent silent failures if action == "find": diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py index 1ad65451..ea89201c 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_prefabs.py @@ -8,7 +8,7 @@ @mcp_for_unity_tool( description="Bridge for prefab management commands (stage control and creation)." ) -async def manage_prefabs( +def manage_prefabs( ctx: Context, action: Annotated[Literal[ "open_stage", @@ -29,7 +29,7 @@ async def manage_prefabs( search_inactive: Annotated[bool, "Include inactive objects when resolving the target name"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing manage_prefabs: {action}") + ctx.info(f"Processing manage_prefabs: {action}") try: params: dict[str, Any] = {"action": action} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py index fbbcf3ca..09494e4a 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_scene.py @@ -6,7 +6,7 @@ @mcp_for_unity_tool(description="Manage Unity scenes") -async def manage_scene( +def manage_scene( ctx: Context, action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."], name: Annotated[str, @@ -16,7 +16,7 @@ async def manage_scene( build_index: Annotated[int, "Build index for load/build settings actions"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing manage_scene: {action}") + ctx.info(f"Processing manage_scene: {action}") try: # Coerce numeric inputs defensively def _coerce_int(value, default=None): diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py index e8b38775..cad6a88c 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_script.py @@ -75,7 +75,7 @@ def _split_uri(uri: str) -> tuple[str, str]: - Lines, columns are 1-indexed - Tabs count as 1 column""" )) -async def apply_text_edits( +def apply_text_edits( ctx: Context, uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."], edits: Annotated[list[dict[str, Any]], "List of edits to apply to the script, i.e. a list of {startLine,startCol,endLine,endCol,newText} (1-indexed!)"], @@ -86,7 +86,7 @@ async def apply_text_edits( options: Annotated[dict[str, Any], "Optional options, used to pass additional options to the script editor"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing apply_text_edits: {uri}") + ctx.info(f"Processing apply_text_edits: {uri}") name, directory = _split_uri(uri) # Normalize common aliases/misuses for resilience: @@ -360,7 +360,7 @@ def create_script( script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None, namespace: Annotated[str, "Namespace for the script"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing create_script: {path}") + ctx.info(f"Processing create_script: {path}") name = os.path.splitext(os.path.basename(path))[0] directory = os.path.dirname(path) # Local validation to avoid round-trips on obviously bad input @@ -396,7 +396,7 @@ def delete_script( uri: Annotated[str, "URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] ) -> dict[str, Any]: """Delete a C# script by URI.""" - await ctx.info(f"Processing delete_script: {uri}") + ctx.info(f"Processing delete_script: {uri}") name, directory = _split_uri(uri) if not directory or directory.split("/")[0].lower() != "assets": return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} @@ -414,7 +414,7 @@ def validate_script( include_diagnostics: Annotated[bool, "Include full diagnostics and summary"] = False ) -> dict[str, Any]: - await ctx.info(f"Processing validate_script: {uri}") + ctx.info(f"Processing validate_script: {uri}") name, directory = _split_uri(uri) if not directory or directory.split("/")[0].lower() != "assets": return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} @@ -451,7 +451,7 @@ def manage_script( "Type hint (e.g., 'MonoBehaviour')"] | None = None, namespace: Annotated[str, "Namespace for the script"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing manage_script: {action}") + ctx.info(f"Processing manage_script: {action}") try: # Prepare parameters for Unity params = { @@ -509,7 +509,7 @@ def manage_script( - guards: header/using guard enabled flag""" )) def manage_script_capabilities(ctx: Context) -> dict[str, Any]: - await ctx.info("Processing manage_script_capabilities") + ctx.info("Processing manage_script_capabilities") try: # Keep in sync with server/Editor ManageScript implementation ops = [ @@ -537,7 +537,7 @@ def get_sha( ctx: Context, uri: Annotated[str, "URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/..."] ) -> dict[str, Any]: - await ctx.info(f"Processing get_sha: {uri}") + ctx.info(f"Processing get_sha: {uri}") try: name, directory = _split_uri(uri) params = {"action": "get_sha", "name": name, "path": directory} diff --git a/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py b/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py index e44611cd..9c199661 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/manage_shader.py @@ -9,7 +9,7 @@ @mcp_for_unity_tool( description="Manages shader scripts in Unity (create, read, update, delete)." ) -async def manage_shader( +def manage_shader( ctx: Context, action: Annotated[Literal['create', 'read', 'update', 'delete'], "Perform CRUD operations on shader scripts."], name: Annotated[str, "Shader name (no .cs extension)"], @@ -17,7 +17,7 @@ async def manage_shader( contents: Annotated[str, "Shader code for 'create'/'update'"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing manage_shader: {action}") + ctx.info(f"Processing manage_shader: {action}") try: # Prepare parameters for Unity params = { diff --git a/MCPForUnity/UnityMcpServer~/src/tools/read_console.py b/MCPForUnity/UnityMcpServer~/src/tools/read_console.py index 4ff1e837..5fc9a096 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/read_console.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/read_console.py @@ -25,7 +25,7 @@ def read_console( include_stacktrace: Annotated[bool, "Include stack traces in output"] | None = None ) -> dict[str, Any]: - await ctx.info(f"Processing read_console: {action}") + ctx.info(f"Processing read_console: {action}") # Set defaults if values are None action = action if action is not None else 'get' types = types if types is not None else ['error', 'warning', 'log'] diff --git a/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py b/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py index ab4aaf07..f28fc589 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/resource_tools.py @@ -142,7 +142,7 @@ async def list_resources( limit: Annotated[int, "Page limit"] = 200, project_root: Annotated[str, "Project path"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing list_resources: {pattern}") + ctx.info(f"Processing list_resources: {pattern}") try: project = _resolve_project_root(project_root) base = (project / under).resolve() @@ -202,7 +202,7 @@ async def read_resource( "The project root directory"] | None = None, request: Annotated[str, "The request ID"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing read_resource: {uri}") + ctx.info(f"Processing read_resource: {uri}") try: # Serve the canonical spec directly when requested (allow bare or with scheme) if uri in ("unity://spec/script-edits", "spec/script-edits", "script-edits"): @@ -357,7 +357,7 @@ async def find_in_file( max_results: Annotated[int, "Cap results to avoid huge payloads"] = 200, ) -> dict[str, Any]: - await ctx.info(f"Processing find_in_file: {uri}") + ctx.info(f"Processing find_in_file: {uri}") try: project = _resolve_project_root(project_root) p = _resolve_safe_path_from_uri(uri, project) diff --git a/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py b/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py index 30980b1f..59fbbc61 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/script_apply_edits.py @@ -354,7 +354,7 @@ def _err(code: str, message: str, *, expected: dict[str, Any] | None = None, rew } ]""" )) -async def script_apply_edits( +def script_apply_edits( ctx: Context, name: Annotated[str, "Name of the script to edit"], path: Annotated[str, "Path to the script to edit under Assets/ directory"], @@ -366,7 +366,7 @@ async def script_apply_edits( namespace: Annotated[str, "Namespace of the script to edit"] | None = None, ) -> dict[str, Any]: - await ctx.info(f"Processing script_apply_edits: {name}") + ctx.info(f"Processing script_apply_edits: {name}") # Normalize locator first so downstream calls target the correct script file. name, path = _normalize_script_locator(name, path) # Normalize unsupported or aliased ops to known structured/text paths From dc6035a64268a771572c85b625f4792b8d01ef03 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 09:40:34 -0400 Subject: [PATCH 38/42] fix: replace tomllib with tomli for Python 3.10 compatibility in telemetry module --- MCPForUnity/UnityMcpServer~/src/telemetry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MCPForUnity/UnityMcpServer~/src/telemetry.py b/MCPForUnity/UnityMcpServer~/src/telemetry.py index 8af77046..b548d1f4 100644 --- a/MCPForUnity/UnityMcpServer~/src/telemetry.py +++ b/MCPForUnity/UnityMcpServer~/src/telemetry.py @@ -24,7 +24,7 @@ from urllib.parse import urlparse import uuid -import tomllib +import tomli try: import httpx @@ -39,9 +39,10 @@ def get_package_version() -> str: """ Open pyproject.toml and parse version + We use the tomli library instead of tomllib to support Python 3.10 """ with open("pyproject.toml", "rb") as f: - data = tomllib.load(f) + data = tomli.load(f) return data["project"]["version"] From da60e385c4c5e15477a58057a582a2ee723468b6 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 09:41:54 -0400 Subject: [PATCH 39/42] Remove confusing comment --- MCPForUnity/UnityMcpServer~/src/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/config.py b/MCPForUnity/UnityMcpServer~/src/config.py index fa2fe377..448d431b 100644 --- a/MCPForUnity/UnityMcpServer~/src/config.py +++ b/MCPForUnity/UnityMcpServer~/src/config.py @@ -16,7 +16,6 @@ class ServerConfig: mcp_port: int = 6500 # Connection settings - # short initial timeout; retries use shorter timeouts connection_timeout: float = 30.0 buffer_size: int = 16 * 1024 * 1024 # 16MB buffer # Framed receive behavior From 1bd970301ae0abe21dc9187d712d771efb278a7a Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 10:55:25 -0400 Subject: [PATCH 40/42] refactor: improve error handling and simplify test retrieval logic in GetTests commands --- .../Editor/Resources/Tests/GetTests.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index 0c8199fc..07a233ab 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -18,21 +18,21 @@ public static class GetTests public static async Task HandleCommand(JObject @params) { McpLog.Info("[GetTests] Retrieving tests for all modes"); + IReadOnlyList> result; - var service = MCPServiceLocator.Tests; - var result = await service.GetTestsAsync(mode: null).ConfigureAwait(true); - var tests = result is List> list - ? list - : new List>(result); - - if (tests == null) + try + { + result = await MCPServiceLocator.Tests.GetTestsAsync(mode: null).ConfigureAwait(true); + } + catch (Exception ex) { + McpLog.Error($"[GetTests] Error retrieving tests: {ex.Message}\n{ex.StackTrace}"); return Response.Error("Failed to retrieve tests"); } - string message = $"Retrieved {tests.Count} tests"; + string message = $"Retrieved {result.Count} tests"; - return Response.Success(message, tests); + return Response.Success(message, result); } } @@ -45,6 +45,7 @@ public static class GetTestsForMode { public static async Task HandleCommand(JObject @params) { + IReadOnlyList> result; string modeStr = @params["mode"]?.ToString(); if (string.IsNullOrEmpty(modeStr)) { @@ -58,19 +59,18 @@ public static async Task HandleCommand(JObject @params) McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value}"); - var service = MCPServiceLocator.Tests; - var result = await service.GetTestsAsync(parsedMode).ConfigureAwait(true); - var tests = result is List> list - ? list - : new List>(result); - - if (tests == null) + try + { + result = await MCPServiceLocator.Tests.GetTestsAsync(parsedMode).ConfigureAwait(true); + } + catch (Exception ex) { + McpLog.Error($"[GetTestsForMode] Error retrieving tests: {ex.Message}\n{ex.StackTrace}"); return Response.Error("Failed to retrieve tests"); } - string message = $"Retrieved {tests.Count} {parsedMode.Value} tests"; - return Response.Success(message, tests); + string message = $"Retrieved {result.Count} {parsedMode.Value} tests"; + return Response.Success(message, result); } } From 3299dcefa5bd22cc58a82e9e16dcbbdd54779bac Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 11:12:46 -0400 Subject: [PATCH 41/42] No cache by default --- MCPForUnity/UnityMcpServer~/src/resources/menu_items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py b/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py index 296fb900..d3724659 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py @@ -17,7 +17,7 @@ async def get_menu_items() -> GetMenuItemsResponse: # Later versions of FastMCP support these as query parameters # See: https://gofastmcp.com/servers/resources#query-parameters params = { - "refresh": False, + "refresh": True, "search": "", } From ffb30a9f4fca6ffabbdd64d6cb1c9c177b60b7d3 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Mon, 13 Oct 2025 11:15:56 -0400 Subject: [PATCH 42/42] docs: remove redundant comment for HandleCommand method in ExecuteMenuItem --- MCPForUnity/Editor/Tools/ExecuteMenuItem.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs index 9e3ac320..503295bc 100644 --- a/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs +++ b/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs @@ -16,9 +16,6 @@ public static class ExecuteMenuItem "File/Quit", }; - /// - /// Routes actions: execute, list, exists, refresh - /// public static object HandleCommand(JObject @params) { McpLog.Info("[ExecuteMenuItem] Handling menu item command");