Add empty test level and Unreal MCP tooling
- L_TestFlight: empty, zero-gravity level for Phase 0 flight testing, set as editor/game default map. - Enable UnrealMCPython + Python Editor Script Plugin, third-party MCP plugin (GenOrca/unreal-mcp v2.2.0) giving direct editor control (actors, blueprints, levels, materials, etc.) alongside Epic's built-in Unreal MCP plugin. - Vendor the mcp-server Python source used to bridge to the plugin. - .mcp.json intentionally gitignored (machine-specific absolute paths).
This commit is contained in:
194
mcp-server/.gitignore
vendored
Normal file
194
mcp-server/.gitignore
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the enitre vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
201
mcp-server/LICENSE
Normal file
201
mcp-server/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
72
mcp-server/README.md
Normal file
72
mcp-server/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# unreal-mcp-server
|
||||
|
||||
unreal-mcp-server is a Python-based server that implements the Model Context Protocol (MCP) for Unreal Engine.
|
||||
It enables smooth communication between MCP clients (e.g., Claude, Cursor, Windsurf) and the Unreal Editor, and is intended to be used together with the Unreal-MCPython Plugin.
|
||||
|
||||
- Demo : [Build 3D Scenes in Unreal Engine with Claude AI | Unreal-MCPython Demo](https://youtu.be/V7KyjzFlBLk?si=sOY1dEVGV2hqi4JC)
|
||||
- Fab Link : [Unreal-MCPython: AI Assistant Plugin for Unreal Editor using Python & MCP](https://fab.com/s/aed5f75d50b2)
|
||||
- Github Link : [GenOrca/unreal-mcpython](https://github.com/GenOrca/unreal-mcpython)
|
||||
|
||||
|
||||
## 🎯 Why Choose Unreal-MCPython?
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/GenOrca/Screenshot/refs/heads/main/unreal-mcp/Screenshot%202025-06-02%20025106.png" width="400">
|
||||
<img src="https://raw.githubusercontent.com/GenOrca/Screenshot/refs/heads/main/unreal-mcp/Screenshot%202025-06-02%20025111.png" width="400">
|
||||
<img src="https://raw.githubusercontent.com/GenOrca/Screenshot/refs/heads/main/unreal-mcp/Screenshot%202025-06-02%20025115.png" width="400">
|
||||
<img src="https://raw.githubusercontent.com/GenOrca/Screenshot/refs/heads/main/unreal-mcp/Screenshot%202025-06-02%20025120.png" width="400">
|
||||
</p>
|
||||
|
||||
- 🧠 **Unreal AI integration** - Direct Claude AI assistance in Unreal Engine
|
||||
- 🔗 **Native MCP protocol support** - Seamless communication between AI and UE
|
||||
- 🎮 **Intelligent game development** - AI-powered asset management and scene manipulation
|
||||
- ⚡ **Smart automation** - Context-aware blueprint scripting with AI guidance
|
||||
- 🎨 **Technical artist focused** - AI assistance for complex production pipelines
|
||||
|
||||
## Key Features
|
||||
|
||||
- MCP server for communication with Unreal Engine
|
||||
- 16 namespace dispatcher tools (actor, material, blueprint, animation, asset, …) exposing 191 actions, each callable as `{action, params}`
|
||||
- Supports Python 3.11 and later
|
||||
|
||||
# Installation
|
||||
|
||||
Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-org/unreal-mcp-server.git
|
||||
cd unreal-mcp-server
|
||||
```
|
||||
|
||||
# Running the Server
|
||||
|
||||
You can start the MCP server with the following command:
|
||||
```bash
|
||||
uv --directory absolute/path/to/unreal-mcp-server run src/unreal_mcp/main.py
|
||||
```
|
||||
|
||||
# Example Configuration (Using Claude, VSCode, Cursor)
|
||||
|
||||
The following is an example configuration for launching the MCP server from Claude, VSCode, or Cursor:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"unreal-mcpython": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"/absolute/path/to/unreal-mcp-server", // e.g., D:/GitHub/unreal-mcp-server
|
||||
"run",
|
||||
"src/unreal_mcp/main.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This configuration approach works similarly across editors like VSCode and Cursor.
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under the Apache-2.0 License. See the LICENSE file for details.
|
||||
153
mcp-server/generate_catalog.py
Normal file
153
mcp-server/generate_catalog.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""
|
||||
Generates the dispatcher action catalog from the plugin's ue_* function signatures.
|
||||
|
||||
The catalog is the single source of truth that list_actions exposes to the LLM.
|
||||
It MUST match the real ue_* signatures exactly, because the dispatcher passes
|
||||
params straight through as ue_func(**params). Hand-transcribing signatures is
|
||||
error-prone, so we extract them via AST instead.
|
||||
|
||||
Usage:
|
||||
python generate_catalog.py # writes src/unreal_mcp/dispatchers/_catalog.py
|
||||
python generate_catalog.py --check # exits 1 if the committed catalog is stale
|
||||
|
||||
Run with --check in CI / validate_tools to catch drift.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MCP_SERVER_DIR = Path(__file__).parent
|
||||
PLUGIN_DIR = MCP_SERVER_DIR.parent / "Plugins" / "UnrealMCPython" / "Content" / "Python" / "UnrealMCPython"
|
||||
OUTPUT = MCP_SERVER_DIR / "src" / "unreal_mcp" / "dispatchers" / "_catalog.py"
|
||||
|
||||
# Domain → plugin action file (module is UnrealMCPython.<file stem>)
|
||||
DOMAINS = [
|
||||
"actor",
|
||||
"anim_blueprint",
|
||||
"animation",
|
||||
"asset",
|
||||
"behavior_tree",
|
||||
"blueprint",
|
||||
"control_rig",
|
||||
"data_table",
|
||||
"editor",
|
||||
"game",
|
||||
"gas",
|
||||
"layer",
|
||||
"level",
|
||||
"level_sequence",
|
||||
"material",
|
||||
"retarget",
|
||||
"static_mesh",
|
||||
"texture",
|
||||
"umg",
|
||||
"util",
|
||||
"vision",
|
||||
]
|
||||
|
||||
# Actions handled by the dispatcher but NOT backed by a ue_* function
|
||||
# (special TCP message types). Documented manually so list_actions shows them.
|
||||
EXTRA_ACTIONS = {
|
||||
"util": {
|
||||
"execute_python": {
|
||||
"params": "code",
|
||||
"doc": "Runs arbitrary Unreal Python code. Full API access; fastest path to prototype new actions.",
|
||||
},
|
||||
"livecoding_compile": {
|
||||
"params": "",
|
||||
"doc": "Triggers C++ Live Coding and waits for the compile result.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _params(fn: ast.FunctionDef) -> str:
|
||||
a = fn.args
|
||||
defaults = a.defaults
|
||||
pad = len(a.args) - len(defaults)
|
||||
parts = []
|
||||
for i, arg in enumerate(a.args):
|
||||
if i >= pad:
|
||||
dv = defaults[i - pad]
|
||||
try:
|
||||
val = ast.literal_eval(dv)
|
||||
except Exception:
|
||||
val = "..."
|
||||
# default None means "required but validated inside" → show name only
|
||||
parts.append(arg.arg if val is None else f"{arg.arg}={val!r}")
|
||||
else:
|
||||
parts.append(arg.arg)
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def _doc(fn: ast.FunctionDef) -> str:
|
||||
d = ast.get_docstring(fn)
|
||||
return d.splitlines()[0].strip() if d else ""
|
||||
|
||||
|
||||
def _extract(domain: str) -> dict:
|
||||
f = PLUGIN_DIR / f"{domain}_actions.py"
|
||||
actions: dict[str, dict] = {}
|
||||
if f.exists():
|
||||
tree = ast.parse(f.read_text(encoding="utf-8"))
|
||||
for n in tree.body:
|
||||
if isinstance(n, ast.FunctionDef) and n.name.startswith("ue_"):
|
||||
action = n.name[3:] # strip ue_
|
||||
actions[action] = {"params": _params(n), "doc": _doc(n)}
|
||||
actions.update(EXTRA_ACTIONS.get(domain, {}))
|
||||
return dict(sorted(actions.items()))
|
||||
|
||||
|
||||
def build() -> dict:
|
||||
return {d: _extract(d) for d in DOMAINS}
|
||||
|
||||
|
||||
def render(catalog: dict) -> str:
|
||||
lines = [
|
||||
"# Copyright (c) 2025 GenOrca. All Rights Reserved.",
|
||||
"#",
|
||||
"# AUTO-GENERATED by generate_catalog.py — do not edit by hand.",
|
||||
"# Regenerate: python generate_catalog.py",
|
||||
"",
|
||||
"CATALOG = {",
|
||||
]
|
||||
for domain, actions in catalog.items():
|
||||
lines.append(f" {domain!r}: {{")
|
||||
for action, info in actions.items():
|
||||
lines.append(f" {action!r}: {{")
|
||||
lines.append(f" {'params'!r}: {info['params']!r},")
|
||||
lines.append(f" {'doc'!r}: {info['doc']!r},")
|
||||
lines.append(" },")
|
||||
lines.append(" },")
|
||||
lines.append("}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
catalog = build()
|
||||
rendered = render(catalog)
|
||||
|
||||
if "--check" in sys.argv:
|
||||
if not OUTPUT.exists():
|
||||
print("FAIL: _catalog.py does not exist. Run generate_catalog.py.")
|
||||
sys.exit(1)
|
||||
current = OUTPUT.read_text(encoding="utf-8")
|
||||
if current.strip() != rendered.strip():
|
||||
print("FAIL: _catalog.py is stale. Run: python generate_catalog.py")
|
||||
sys.exit(1)
|
||||
total = sum(len(a) for a in catalog.values())
|
||||
print(f"OK: catalog in sync ({total} actions across {len(catalog)} domains).")
|
||||
sys.exit(0)
|
||||
|
||||
OUTPUT.write_text(rendered, encoding="utf-8")
|
||||
total = sum(len(a) for a in catalog.values())
|
||||
print(f"Wrote {OUTPUT} ({total} actions across {len(catalog)} domains).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
mcp-server/pyproject.toml
Normal file
34
mcp-server/pyproject.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "unrealmcp"
|
||||
version = "2.2.0"
|
||||
description = "MCP Server for Unreal Engine integration"
|
||||
authors = [
|
||||
{name = "Jinhyung Ahn", email = "zenoengine@gmail.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = "Apache-2.0"
|
||||
dependencies = [
|
||||
"fastmcp",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
unrealmcp = "unreal_mcp.main:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
21
mcp-server/setup.py
Normal file
21
mcp-server/setup.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="unreal-mcp",
|
||||
version="1.0.0",
|
||||
description="MCP Server for Unreal Engine integration",
|
||||
author="Your Name",
|
||||
author_email="your.email@example.com",
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
python_requires=">=3.8",
|
||||
install_requires=[
|
||||
"fastmcp>=1.0.0",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"unreal-mcp=unreal_mcp.server:run_server",
|
||||
"unreal-mcp-test=main:test_server"
|
||||
],
|
||||
},
|
||||
)
|
||||
249
mcp-server/src/unreal_mcp/core.py
Normal file
249
mcp-server/src/unreal_mcp/core.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
import socket
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Custom Exception classes
|
||||
class ToolInputError(Exception):
|
||||
pass
|
||||
|
||||
class UnrealExecutionError(Exception):
|
||||
def __init__(self, message, details=None):
|
||||
super().__init__(message)
|
||||
self.details = details if details is not None else {}
|
||||
|
||||
|
||||
async def send_unreal_action(action_module: str, params: dict) -> dict:
|
||||
"""
|
||||
Convention-based wrapper for send_to_unreal.
|
||||
Auto-derives action_name from the calling function's name.
|
||||
Convention: caller 'foo_bar' → action 'ue_foo_bar'
|
||||
Also includes standard error handling.
|
||||
"""
|
||||
caller_name = sys._getframe(1).f_code.co_name
|
||||
action_name = f"ue_{caller_name}"
|
||||
try:
|
||||
return await send_to_unreal(action_module, action_name, params)
|
||||
except UnrealExecutionError as e:
|
||||
return {"success": False, "message": str(e), "details": e.details}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"An unexpected error occurred: {str(e)}"}
|
||||
|
||||
def _unwrap_result(response: dict) -> dict:
|
||||
"""
|
||||
The TCP server's python_call path double-wraps the action's JSON return.
|
||||
The C++ server runs `print(execute_action(...))` and captures stdout into a
|
||||
string "result" field, so the wire shape is:
|
||||
|
||||
{"success": <python-exec ok>, "message": ..., "result": "<action json string>"}
|
||||
|
||||
The OUTER "success" is whether Python *ran*, NOT whether the action
|
||||
succeeded. The real action dict (with its own "success", "actor_label", ...)
|
||||
is the JSON string in "result". Unwrap it so callers get the action dict.
|
||||
"""
|
||||
if not isinstance(response, dict):
|
||||
return response
|
||||
inner = response.get("result")
|
||||
if isinstance(inner, str):
|
||||
try:
|
||||
parsed = json.loads(inner)
|
||||
except (ValueError, TypeError):
|
||||
return response
|
||||
if isinstance(parsed, (dict, list)):
|
||||
return parsed
|
||||
return response
|
||||
|
||||
|
||||
# Core send_to_unreal function
|
||||
async def send_to_unreal(action_module: str, action_name: str, params: dict) -> dict:
|
||||
"""
|
||||
Sends a command to the Unreal Engine Python script via socket communication.
|
||||
Args:
|
||||
action_module (str): The Python module in Unreal (e.g., 'actor_actions').
|
||||
action_name (str): The function name in the module (e.g., 'ue_spawn_actor_from_class').
|
||||
params (dict): A dictionary of parameters for the action.
|
||||
Returns:
|
||||
dict: The JSON response from Unreal.
|
||||
Raises:
|
||||
UnrealExecutionError: If any error occurs during socket communication or JSON processing.
|
||||
ToolInputError: If there's an issue with the input that can be determined client-side (though less common here).
|
||||
"""
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 12029
|
||||
command = {
|
||||
"type": "python_call",
|
||||
"module": action_module,
|
||||
"function": action_name,
|
||||
"args": params
|
||||
}
|
||||
response_str = ""
|
||||
try:
|
||||
json_str = json.dumps(command, ensure_ascii=False)
|
||||
message_bytes = json_str.encode('utf-8')
|
||||
|
||||
# Using asyncio for socket communication would be better for a fully async server,
|
||||
# but standard socket is used here as per existing structure.
|
||||
# If FastMCP's .run() uses an async server like uvicorn, this blocking call
|
||||
# will run in a thread pool.
|
||||
with socket.create_connection((HOST, PORT), timeout=30) as sock:
|
||||
sock.sendall(message_bytes)
|
||||
response_buffer = b''
|
||||
while True:
|
||||
chunk = sock.recv(16384)
|
||||
if not chunk:
|
||||
break
|
||||
response_buffer += chunk
|
||||
|
||||
if not response_buffer:
|
||||
raise UnrealExecutionError("No response received from Unreal.", details={"host": HOST, "port": PORT})
|
||||
|
||||
response_str = response_buffer.decode('utf-8')
|
||||
response_json = json.loads(response_str)
|
||||
|
||||
# Outer "success" is python-exec success. False = Python failed to run
|
||||
# (bad module/function, syntax error) → propagate as an error.
|
||||
if isinstance(response_json, dict) and response_json.get("success") is False:
|
||||
raise UnrealExecutionError(
|
||||
response_json.get("message", "Unknown error from Unreal action."),
|
||||
details=response_json.get("details")
|
||||
)
|
||||
|
||||
# Python ran; unwrap the action's real result out of the "result" string.
|
||||
# The action's own success/failure is preserved in the unwrapped dict.
|
||||
return _unwrap_result(response_json)
|
||||
|
||||
except socket.timeout:
|
||||
raise UnrealExecutionError(f"Socket timeout ({HOST}:{PORT}): No response from Unreal.", details={"host": HOST, "port": PORT})
|
||||
except ConnectionRefusedError:
|
||||
raise UnrealExecutionError(f"Connection refused ({HOST}:{PORT}). Ensure Unreal MCPython TCP server is active.", details={"host": HOST, "port": PORT})
|
||||
except json.JSONDecodeError as je:
|
||||
raise UnrealExecutionError(f"Failed to decode JSON response from Unreal: {je}. Raw response: '{response_str}'", details={"host": HOST, "port": PORT, "raw_response": response_str})
|
||||
except socket.error as se:
|
||||
raise UnrealExecutionError(f"Socket error ({HOST}:{PORT}): {se}", details={"host": HOST, "port": PORT})
|
||||
except UnrealExecutionError: # Re-raise if it's already our specific error type
|
||||
raise
|
||||
except Exception as e: # Catch any other unexpected errors
|
||||
raise UnrealExecutionError(f"An unexpected error occurred in send_to_unreal ({HOST}:{PORT}): {type(e).__name__} - {e}", details={"host": HOST, "port": PORT, "error_type": type(e).__name__})
|
||||
|
||||
|
||||
async def send_python_exec(code: str) -> dict:
|
||||
"""
|
||||
Sends raw Python code to the Unreal Engine TCP server for execution.
|
||||
Uses the existing "type": "python" dispatch path.
|
||||
The C++ server executes the code and captures print() output.
|
||||
"""
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 12029
|
||||
TIMEOUT = 30
|
||||
command = {"type": "python", "code": code}
|
||||
response_str = ""
|
||||
try:
|
||||
json_str = json.dumps(command, ensure_ascii=False)
|
||||
message_bytes = json_str.encode('utf-8')
|
||||
|
||||
with socket.create_connection((HOST, PORT), timeout=TIMEOUT) as sock:
|
||||
sock.sendall(message_bytes)
|
||||
response_buffer = b''
|
||||
while True:
|
||||
chunk = sock.recv(16384)
|
||||
if not chunk:
|
||||
break
|
||||
response_buffer += chunk
|
||||
|
||||
if not response_buffer:
|
||||
raise UnrealExecutionError(
|
||||
"No response received from Unreal for Python execution.",
|
||||
details={"host": HOST, "port": PORT}
|
||||
)
|
||||
|
||||
response_str = response_buffer.decode('utf-8')
|
||||
response_json = json.loads(response_str)
|
||||
return response_json
|
||||
|
||||
except socket.timeout:
|
||||
raise UnrealExecutionError(
|
||||
f"Socket timeout ({HOST}:{PORT}): Python execution did not complete within {TIMEOUT}s.",
|
||||
details={"host": HOST, "port": PORT}
|
||||
)
|
||||
except ConnectionRefusedError:
|
||||
raise UnrealExecutionError(
|
||||
f"Connection refused ({HOST}:{PORT}). Ensure Unreal MCPython TCP server is active.",
|
||||
details={"host": HOST, "port": PORT}
|
||||
)
|
||||
except json.JSONDecodeError as je:
|
||||
raise UnrealExecutionError(
|
||||
f"Failed to decode JSON response: {je}. Raw: '{response_str}'",
|
||||
details={"host": HOST, "port": PORT, "raw_response": response_str}
|
||||
)
|
||||
except UnrealExecutionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise UnrealExecutionError(
|
||||
f"Unexpected error during Python execution: {type(e).__name__} - {e}",
|
||||
details={"host": HOST, "port": PORT, "error_type": type(e).__name__}
|
||||
)
|
||||
|
||||
|
||||
async def send_livecoding_compile() -> dict:
|
||||
"""
|
||||
Sends a livecoding_compile command to the Unreal Engine TCP server.
|
||||
Triggers C++ hot-reload via the LiveCoding module.
|
||||
Waits for compilation to complete before returning the result.
|
||||
"""
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 12029
|
||||
TIMEOUT = 180
|
||||
command = {"type": "livecoding_compile"}
|
||||
response_str = ""
|
||||
try:
|
||||
json_str = json.dumps(command, ensure_ascii=False)
|
||||
message_bytes = json_str.encode('utf-8')
|
||||
|
||||
with socket.create_connection((HOST, PORT), timeout=TIMEOUT) as sock:
|
||||
sock.sendall(message_bytes)
|
||||
response_buffer = b''
|
||||
while True:
|
||||
chunk = sock.recv(16384)
|
||||
if not chunk:
|
||||
break
|
||||
response_buffer += chunk
|
||||
|
||||
if not response_buffer:
|
||||
raise UnrealExecutionError(
|
||||
"No response received from Unreal for LiveCoding compile.",
|
||||
details={"host": HOST, "port": PORT}
|
||||
)
|
||||
|
||||
response_str = response_buffer.decode('utf-8')
|
||||
response_json = json.loads(response_str)
|
||||
|
||||
if isinstance(response_json, dict) and response_json.get("success") is False:
|
||||
raise UnrealExecutionError(
|
||||
response_json.get("message", "LiveCoding compile failed."),
|
||||
details=response_json
|
||||
)
|
||||
return response_json
|
||||
|
||||
except socket.timeout:
|
||||
raise UnrealExecutionError(
|
||||
f"Socket timeout ({HOST}:{PORT}): LiveCoding compilation did not complete within {TIMEOUT}s.",
|
||||
details={"host": HOST, "port": PORT}
|
||||
)
|
||||
except ConnectionRefusedError:
|
||||
raise UnrealExecutionError(
|
||||
f"Connection refused ({HOST}:{PORT}). Ensure Unreal MCPython TCP server is active.",
|
||||
details={"host": HOST, "port": PORT}
|
||||
)
|
||||
except json.JSONDecodeError as je:
|
||||
raise UnrealExecutionError(
|
||||
f"Failed to decode JSON response: {je}. Raw: '{response_str}'",
|
||||
details={"host": HOST, "port": PORT, "raw_response": response_str}
|
||||
)
|
||||
except UnrealExecutionError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise UnrealExecutionError(
|
||||
f"Unexpected error during LiveCoding compile: {type(e).__name__} - {e}",
|
||||
details={"host": HOST, "port": PORT, "error_type": type(e).__name__}
|
||||
)
|
||||
131
mcp-server/src/unreal_mcp/dispatcher.py
Normal file
131
mcp-server/src/unreal_mcp/dispatcher.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""
|
||||
Namespace dispatcher — thin assembler.
|
||||
|
||||
One MCP tool per domain. Each accepts (action, params) and routes to the
|
||||
Unreal TCP server as ue_<action>(**params). action='list_actions' returns the
|
||||
domain's action catalog (param names + one-line docs).
|
||||
|
||||
The catalog is AUTO-GENERATED from the plugin's ue_* signatures
|
||||
(see generate_catalog.py). Param names are therefore guaranteed to match —
|
||||
no hand transcription. This file never grows when actions are added.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
from fastmcp import FastMCP, Image
|
||||
|
||||
from unreal_mcp.core import send_to_unreal, UnrealExecutionError, send_python_exec, send_livecoding_compile
|
||||
from unreal_mcp.dispatchers._catalog import CATALOG
|
||||
|
||||
dispatcher_mcp = FastMCP(
|
||||
name="UnrealMCP",
|
||||
description=(
|
||||
"Unreal Engine MCP via namespace dispatchers. "
|
||||
"Each domain tool accepts (action, params). "
|
||||
"Pass action='list_actions' to any tool to get available actions and parameter docs."
|
||||
)
|
||||
)
|
||||
|
||||
# Domains using standard python_call routing. util and vision have hand-written
|
||||
# handlers (special TCP types / MCP Image return).
|
||||
_SPECIAL_DOMAINS = {"util", "vision"}
|
||||
_STANDARD_DOMAINS = [d for d in CATALOG if d not in _SPECIAL_DOMAINS]
|
||||
|
||||
|
||||
def _module(domain: str) -> str:
|
||||
return f"UnrealMCPython.{domain}_actions"
|
||||
|
||||
|
||||
async def _dispatch(domain: str, action: str, params: dict) -> dict:
|
||||
if action == "list_actions":
|
||||
return {"success": True, "domain": domain, "actions": CATALOG[domain]}
|
||||
if action not in CATALOG[domain]:
|
||||
return {"success": False, "message": f"Unknown action '{action}'. Available: {list(CATALOG[domain])}"}
|
||||
try:
|
||||
return await send_to_unreal(_module(domain), f"ue_{action}", params)
|
||||
except UnrealExecutionError as e:
|
||||
return {"success": False, "message": str(e), "details": getattr(e, "details", {})}
|
||||
except Exception as e:
|
||||
return {"success": False, "message": f"Unexpected error: {e}"}
|
||||
|
||||
|
||||
def _desc(domain: str) -> str:
|
||||
actions = ", ".join(CATALOG[domain])
|
||||
return f"Unreal {domain} tools. Actions: {actions}. Pass action='list_actions' for parameter docs."
|
||||
|
||||
|
||||
def _make_handler(domain: str):
|
||||
async def handler(
|
||||
action: Annotated[str, Field(description="Action name. Use 'list_actions' for full parameter docs.")],
|
||||
params: Annotated[dict, Field(description="Action parameters (keys must match the action's documented params).")] = {}
|
||||
) -> dict:
|
||||
return await _dispatch(domain, action, params)
|
||||
handler.__name__ = domain
|
||||
return handler
|
||||
|
||||
|
||||
for _domain in _STANDARD_DOMAINS:
|
||||
dispatcher_mcp.tool(name=_domain, description=_desc(_domain))(_make_handler(_domain))
|
||||
|
||||
|
||||
# ─── util: special routing (execute_python / livecoding_compile use dedicated TCP types) ──
|
||||
|
||||
@dispatcher_mcp.tool(name="util", description=_desc("util"))
|
||||
async def util(
|
||||
action: Annotated[str, Field(description="Action name. Use 'list_actions' for full parameter docs.")],
|
||||
params: Annotated[dict, Field(description="Action parameters. execute_python: {code}. get_output_log: {line_count, keyword}.")] = {}
|
||||
) -> dict:
|
||||
if action == "list_actions":
|
||||
return {"success": True, "domain": "util", "actions": CATALOG["util"]}
|
||||
|
||||
if action == "execute_python":
|
||||
code = params.get("code", "")
|
||||
if not code:
|
||||
return {"success": False, "message": "params.code is required"}
|
||||
try:
|
||||
return await send_python_exec(code)
|
||||
except UnrealExecutionError as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
if action == "livecoding_compile":
|
||||
try:
|
||||
return await send_livecoding_compile()
|
||||
except UnrealExecutionError as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
if action in CATALOG["util"]:
|
||||
try:
|
||||
return await send_to_unreal(_module("util"), f"ue_{action}", params)
|
||||
except UnrealExecutionError as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
return {"success": False, "message": f"Unknown action '{action}'. Available: {list(CATALOG['util'])}"}
|
||||
|
||||
|
||||
# ─── vision: special routing (returns an MCP Image for captures) ────────────────
|
||||
|
||||
@dispatcher_mcp.tool(name="vision", description=_desc("vision"))
|
||||
async def vision(
|
||||
action: Annotated[str, Field(description="Action name. Use 'list_actions' for full parameter docs.")],
|
||||
params: Annotated[dict, Field(description="Action parameters. capture_viewport: {width, height, fov}.")] = {}
|
||||
) -> "Image | dict":
|
||||
if action == "list_actions":
|
||||
return {"success": True, "domain": "vision", "actions": CATALOG["vision"]}
|
||||
|
||||
if action not in CATALOG["vision"]:
|
||||
return {"success": False, "message": f"Unknown action '{action}'. Available: {list(CATALOG['vision'])}"}
|
||||
|
||||
try:
|
||||
result = await send_to_unreal(_module("vision"), f"ue_{action}", params)
|
||||
except UnrealExecutionError as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
# Capture actions return base64 PNG in 'image_data' → hand back an MCP Image.
|
||||
if isinstance(result, dict):
|
||||
img_b64 = result.get("image_data")
|
||||
if result.get("success") and img_b64:
|
||||
return Image(data=base64.b64decode(img_b64), format="png")
|
||||
return result
|
||||
0
mcp-server/src/unreal_mcp/dispatchers/__init__.py
Normal file
0
mcp-server/src/unreal_mcp/dispatchers/__init__.py
Normal file
1061
mcp-server/src/unreal_mcp/dispatchers/_catalog.py
Normal file
1061
mcp-server/src/unreal_mcp/dispatchers/_catalog.py
Normal file
File diff suppressed because it is too large
Load Diff
24
mcp-server/src/unreal_mcp/main.py
Normal file
24
mcp-server/src/unreal_mcp/main.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from unreal_mcp.server import main_mcp, run_server
|
||||
from fastmcp import Client
|
||||
|
||||
async def test_server():
|
||||
print("Testing Unreal MCP Server...")
|
||||
tools = await main_mcp.get_tools()
|
||||
print(f"Available tools: {list(tools.keys())}")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--test":
|
||||
asyncio.run(test_server())
|
||||
else:
|
||||
run_server()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
mcp-server/src/unreal_mcp/server.py
Normal file
13
mcp-server/src/unreal_mcp/server.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
from unreal_mcp.dispatcher import dispatcher_mcp
|
||||
|
||||
# Public name kept stable for main.py and external imports.
|
||||
main_mcp = dispatcher_mcp
|
||||
|
||||
def run_server():
|
||||
"""Entry point function for the Unreal MCP Server"""
|
||||
main_mcp.run(transport="stdio")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server()
|
||||
45
mcp-server/tests/test_core.py
Normal file
45
mcp-server/tests/test_core.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""Unit tests for the TCP response unwrapping (offline, no Unreal)."""
|
||||
|
||||
from unreal_mcp.core import _unwrap_result
|
||||
|
||||
|
||||
def test_unwraps_action_dict_from_result_string():
|
||||
wire = {
|
||||
"success": True, # python-exec success, NOT action success
|
||||
"message": "Python command executed successfully.",
|
||||
"result": '{"success": true, "actor_label": "PointLight_2"}',
|
||||
}
|
||||
out = _unwrap_result(wire)
|
||||
assert out == {"success": True, "actor_label": "PointLight_2"}
|
||||
|
||||
|
||||
def test_unwrapped_inner_failure_is_surfaced():
|
||||
"""Action failure (inner success=False) must survive unwrapping as data."""
|
||||
wire = {
|
||||
"success": True, # python ran fine...
|
||||
"result": '{"success": false, "message": "Actor not found"}',
|
||||
}
|
||||
out = _unwrap_result(wire)
|
||||
assert out["success"] is False
|
||||
assert out["message"] == "Actor not found"
|
||||
|
||||
|
||||
def test_unwraps_list_result():
|
||||
wire = {"success": True, "result": '[1, 2, 3]'}
|
||||
assert _unwrap_result(wire) == [1, 2, 3]
|
||||
|
||||
|
||||
def test_non_json_result_returns_original():
|
||||
wire = {"success": True, "result": "not json at all"}
|
||||
assert _unwrap_result(wire) == wire
|
||||
|
||||
|
||||
def test_missing_result_returns_original():
|
||||
wire = {"success": True, "message": "ok"}
|
||||
assert _unwrap_result(wire) == wire
|
||||
|
||||
|
||||
def test_non_dict_passthrough():
|
||||
assert _unwrap_result("raw") == "raw"
|
||||
88
mcp-server/tests/test_coverage.py
Normal file
88
mcp-server/tests/test_coverage.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""
|
||||
Action coverage enforcement (offline, no Unreal needed).
|
||||
|
||||
Every catalog action must be exercised by an in-editor unittest
|
||||
(Plugins/.../tests/test_<domain>.py references "ue_<action>"), OR be listed
|
||||
in KNOWN_UNTESTED below as acknowledged technical debt.
|
||||
|
||||
Why: the dispatcher auto-exposes any ue_* function. Without this gate, a new
|
||||
action could ship with zero behavior test and all other gates stay green.
|
||||
This makes "add an action without a test" a conscious, reviewable choice
|
||||
(you must edit KNOWN_UNTESTED) rather than a silent gap.
|
||||
|
||||
KNOWN_UNTESTED is debt to shrink, not to grow. The stale-entry check fails if
|
||||
an allowlisted action becomes tested or disappears, so the list self-cleans.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from unreal_mcp.dispatchers._catalog import CATALOG
|
||||
|
||||
PLUGIN_TESTS = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "Plugins" / "UnrealMCPython" / "Content" / "Python" / "UnrealMCPython" / "tests"
|
||||
)
|
||||
|
||||
# Actions not routed as ue_<action> over TCP — covered by mcp-server pytest instead.
|
||||
SPECIAL = {"util": {"execute_python", "livecoding_compile"}}
|
||||
|
||||
# ── Technical debt: actions with no in-editor behavior test yet. SHRINK over time. ──
|
||||
# Adding a new action? Write a test in test_<domain>.py instead of adding it here.
|
||||
KNOWN_UNTESTED: dict[str, set[str]] = {
|
||||
# PIE start/stop change the editor play mode asynchronously; running them in the
|
||||
# headless suite (which never ticks between calls) would leave the editor in PIE.
|
||||
# Verified manually through the MCP chain instead.
|
||||
"util": {"start_pie", "stop_pie"},
|
||||
# save_current_level can raise a modal Save-As dialog on an untitled level,
|
||||
# which would hang the headless suite; save_all_levels is grouped with it.
|
||||
# Verified manually instead.
|
||||
"level": {"save_current_level", "save_all_levels"},
|
||||
}
|
||||
|
||||
|
||||
def _referenced(domain: str) -> set[str]:
|
||||
tf = PLUGIN_TESTS / f"test_{domain}.py"
|
||||
if not tf.exists():
|
||||
return set()
|
||||
return set(re.findall(r"ue_(\w+)", tf.read_text(encoding="utf-8")))
|
||||
|
||||
|
||||
def _allowed(domain: str) -> set[str]:
|
||||
return SPECIAL.get(domain, set()) | KNOWN_UNTESTED.get(domain, set())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain", sorted(CATALOG))
|
||||
def test_every_action_is_tested_or_allowlisted(domain):
|
||||
referenced = _referenced(domain)
|
||||
untested = [
|
||||
a for a in CATALOG[domain]
|
||||
if a not in referenced and a not in _allowed(domain)
|
||||
]
|
||||
assert not untested, (
|
||||
f"{domain}: these actions have no in-editor test and are not allowlisted: "
|
||||
f"{untested}. Add a test in test_{domain}.py, or (consciously) add them to "
|
||||
f"KNOWN_UNTESTED in test_coverage.py."
|
||||
)
|
||||
|
||||
|
||||
def test_no_stale_allowlist_entries():
|
||||
"""KNOWN_UNTESTED must not contain actions that are now tested or no longer exist."""
|
||||
stale = []
|
||||
for domain, actions in KNOWN_UNTESTED.items():
|
||||
referenced = _referenced(domain)
|
||||
for a in actions:
|
||||
if a not in CATALOG.get(domain, {}):
|
||||
stale.append(f"{domain}.{a} (not in catalog)")
|
||||
elif a in referenced:
|
||||
stale.append(f"{domain}.{a} (now tested — remove from allowlist)")
|
||||
assert not stale, f"Stale KNOWN_UNTESTED entries: {stale}"
|
||||
|
||||
|
||||
def test_plugin_tests_dir_exists():
|
||||
"""Guard: if the layout moves, fail loudly instead of silently passing coverage."""
|
||||
assert PLUGIN_TESTS.is_dir(), f"Plugin tests dir not found: {PLUGIN_TESTS}"
|
||||
177
mcp-server/tests/test_dispatcher.py
Normal file
177
mcp-server/tests/test_dispatcher.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""
|
||||
Offline routing tests for the namespace dispatcher.
|
||||
|
||||
These do NOT need Unreal running. They mock the TCP layer (send_to_unreal /
|
||||
send_python_exec / send_livecoding_compile) and assert that each domain tool
|
||||
routes (action, params) to the correct module + ue_<action> + params.
|
||||
|
||||
Run:
|
||||
cd mcp-server && uv run pytest
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
import unreal_mcp.dispatcher as disp
|
||||
from unreal_mcp.dispatchers._catalog import CATALOG
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def recorder(monkeypatch):
|
||||
"""Replace send_to_unreal with an async recorder; return the call log."""
|
||||
calls = []
|
||||
|
||||
async def fake_send_to_unreal(module, fn, params):
|
||||
calls.append((module, fn, params))
|
||||
return {"success": True, "echo": {"module": module, "fn": fn, "params": params}}
|
||||
|
||||
monkeypatch.setattr(disp, "send_to_unreal", fake_send_to_unreal)
|
||||
return calls
|
||||
|
||||
|
||||
# ─── entrypoint smoke ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_entrypoints_import():
|
||||
"""main.py and server.py must import cleanly (catches removed public names)."""
|
||||
import unreal_mcp.server as server
|
||||
import unreal_mcp.main as main
|
||||
assert hasattr(server, "main_mcp")
|
||||
assert hasattr(server, "run_server")
|
||||
assert callable(main.main)
|
||||
|
||||
|
||||
# ─── tool registration ────────────────────────────────────────────────────────
|
||||
|
||||
def test_domain_tools_match_catalog():
|
||||
"""Exactly one MCP tool per catalog domain — no more, no less."""
|
||||
tools = disp.dispatcher_mcp._tool_manager.list_tools()
|
||||
names = sorted(t.name for t in tools)
|
||||
assert names == sorted(CATALOG.keys())
|
||||
|
||||
|
||||
# ─── standard routing ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_standard_routing_module_fn_params(recorder):
|
||||
result = run(disp._dispatch("actor", "set_location",
|
||||
{"actor_label": "Cube", "location": [0, 0, 100]}))
|
||||
assert result["success"] is True
|
||||
assert recorder == [
|
||||
("UnrealMCPython.actor_actions", "ue_set_location",
|
||||
{"actor_label": "Cube", "location": [0, 0, 100]})
|
||||
]
|
||||
|
||||
|
||||
def _standard_action_pairs():
|
||||
"""Every (domain, action) routed via the standard path (excludes hand-written domains)."""
|
||||
pairs = []
|
||||
for domain in CATALOG:
|
||||
if domain in ("util", "vision"):
|
||||
continue # special-cased handlers
|
||||
for action in CATALOG[domain]:
|
||||
pairs.append((domain, action))
|
||||
return pairs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain,action", _standard_action_pairs())
|
||||
def test_every_catalog_action_routes(domain, action, recorder):
|
||||
"""Full coverage: each catalog action routes to its module + ue_<action>."""
|
||||
run(disp._dispatch(domain, action, {}))
|
||||
module, fn, _ = recorder[0]
|
||||
assert module == f"UnrealMCPython.{domain}_actions"
|
||||
assert fn == f"ue_{action}"
|
||||
|
||||
|
||||
def test_params_passed_through_untouched(recorder):
|
||||
payload = {"asset_path": "/Game/BP", "graph_name": "EventGraph", "node_json": {"type": "Branch"}}
|
||||
run(disp._dispatch("blueprint", "add_blueprint_node", payload))
|
||||
assert recorder[0][2] == payload # dispatcher must not mutate/translate params
|
||||
|
||||
|
||||
# ─── list_actions ─────────────────────────────────────────────────────────────
|
||||
|
||||
def test_list_actions_returns_catalog(recorder):
|
||||
result = run(disp._dispatch("material", "list_actions", {}))
|
||||
assert result["success"] is True
|
||||
assert result["domain"] == "material"
|
||||
assert result["actions"] == CATALOG["material"]
|
||||
assert recorder == [] # list_actions must NOT hit the TCP layer
|
||||
|
||||
|
||||
def test_list_actions_entries_have_params_and_doc():
|
||||
for domain, actions in CATALOG.items():
|
||||
for action, info in actions.items():
|
||||
assert "params" in info and "doc" in info, f"{domain}.{action} missing keys"
|
||||
|
||||
|
||||
# ─── error handling ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_unknown_action_errors_without_tcp(recorder):
|
||||
result = run(disp._dispatch("actor", "fly_to_the_moon", {}))
|
||||
assert result["success"] is False
|
||||
assert "Unknown action" in result["message"]
|
||||
assert recorder == [] # never reaches the TCP layer
|
||||
|
||||
|
||||
# ─── util special routing ─────────────────────────────────────────────────────
|
||||
|
||||
def test_util_execute_python(monkeypatch):
|
||||
seen = {}
|
||||
|
||||
async def fake_exec(code):
|
||||
seen["code"] = code
|
||||
return {"success": True}
|
||||
|
||||
monkeypatch.setattr(disp, "send_python_exec", fake_exec)
|
||||
result = run(disp.util.fn(action="execute_python", params={"code": "import unreal"}))
|
||||
assert result["success"] is True
|
||||
assert seen["code"] == "import unreal"
|
||||
|
||||
|
||||
def test_util_execute_python_requires_code(monkeypatch):
|
||||
async def fake_exec(code):
|
||||
raise AssertionError("should not be called when code is missing")
|
||||
|
||||
monkeypatch.setattr(disp, "send_python_exec", fake_exec)
|
||||
result = run(disp.util.fn(action="execute_python", params={}))
|
||||
assert result["success"] is False
|
||||
assert "code is required" in result["message"]
|
||||
|
||||
|
||||
def test_util_livecoding_compile(monkeypatch):
|
||||
called = {"n": 0}
|
||||
|
||||
async def fake_compile():
|
||||
called["n"] += 1
|
||||
return {"success": True}
|
||||
|
||||
monkeypatch.setattr(disp, "send_livecoding_compile", fake_compile)
|
||||
result = run(disp.util.fn(action="livecoding_compile", params={}))
|
||||
assert result["success"] is True
|
||||
assert called["n"] == 1
|
||||
|
||||
|
||||
def test_util_get_output_log_routes_to_ue_function(recorder):
|
||||
run(disp.util.fn(action="get_output_log", params={"line_count": 20}))
|
||||
assert recorder == [
|
||||
("UnrealMCPython.util_actions", "ue_get_output_log", {"line_count": 20})
|
||||
]
|
||||
|
||||
|
||||
def test_util_list_actions(recorder):
|
||||
result = run(disp.util.fn(action="list_actions", params={}))
|
||||
assert result["actions"] == CATALOG["util"]
|
||||
assert recorder == []
|
||||
|
||||
|
||||
def test_util_unknown_action(recorder):
|
||||
result = run(disp.util.fn(action="nope", params={}))
|
||||
assert result["success"] is False
|
||||
assert "Unknown action" in result["message"]
|
||||
222
mcp-server/tests/test_e2e.py
Normal file
222
mcp-server/tests/test_e2e.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""
|
||||
End-to-end tests: MCP server dispatcher -> real TCP -> C++ server -> ue_* -> back.
|
||||
|
||||
Unlike test_dispatcher.py (which mocks the TCP layer), these run the FULL chain
|
||||
with NO mocks. They prove the actual scenario an MCP client triggers:
|
||||
routing + socket round-trip + C++ dispatch + response unwrapping.
|
||||
|
||||
Requires a running Unreal editor with the UnrealMCPython TCP server on :12029.
|
||||
If the port is not reachable, the whole module is skipped (so CI / offline runs
|
||||
stay green; run locally with the editor open to exercise these).
|
||||
|
||||
Run:
|
||||
cd mcp-server && uv run --extra dev pytest tests/test_e2e.py -v
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
import unreal_mcp.dispatcher as disp
|
||||
from unreal_mcp.dispatchers._catalog import CATALOG
|
||||
|
||||
HOST, PORT = "127.0.0.1", 12029
|
||||
|
||||
# Actions excluded from the exhaustive empty-param round-trip:
|
||||
# execute_python — needs {code}; empty-param hits a dispatcher guard, not TCP
|
||||
# livecoding_compile — triggers a real C++ compile (slow, side-effecting)
|
||||
# start_pie/stop_pie — change editor PIE mode (no params to guard); would leave
|
||||
# the editor in PIE during the sweep
|
||||
_EXCLUDE = {
|
||||
("util", "execute_python"),
|
||||
("util", "livecoding_compile"),
|
||||
("util", "start_pie"),
|
||||
("util", "stop_pie"),
|
||||
# save_current_level can raise a modal Save-As dialog on an untitled level.
|
||||
("level", "save_current_level"),
|
||||
("level", "save_all_levels"),
|
||||
# vision returns an MCP Image (not a dict) — covered by a dedicated E2E test below.
|
||||
("vision", "capture_viewport"),
|
||||
}
|
||||
|
||||
|
||||
def _editor_reachable() -> bool:
|
||||
try:
|
||||
with socket.create_connection((HOST, PORT), timeout=0.5):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not _editor_reachable(),
|
||||
reason=f"Unreal TCP server not reachable on {HOST}:{PORT} (open the editor to run E2E).",
|
||||
)
|
||||
|
||||
# Editor-crash guard: if the editor was up when the module started but dies
|
||||
# mid-suite, every remaining test must FAIL (not skip, and not "pass" via a
|
||||
# connection-error dict that happens to carry a 'success' key). A green E2E run
|
||||
# must mean the editor survived the whole sweep — otherwise a `pytest && ...`
|
||||
# release chain would happily commit/PR on top of a crash.
|
||||
_EDITOR_WAS_UP = _editor_reachable()
|
||||
_CONNECTION_ERROR_MARKERS = ("Connection refused", "ConnectionReset", "Socket timeout",
|
||||
"No response received", "[WinError")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _fail_if_editor_crashed():
|
||||
if _EDITOR_WAS_UP and not _editor_reachable():
|
||||
pytest.fail(f"Unreal editor crashed during the E2E suite "
|
||||
f"({HOST}:{PORT} no longer reachable). Investigate before merging.")
|
||||
yield
|
||||
|
||||
|
||||
def _assert_not_connection_error(r, label):
|
||||
if isinstance(r, dict):
|
||||
msg = str(r.get("message", ""))
|
||||
assert not any(m in msg for m in _CONNECTION_ERROR_MARKERS), \
|
||||
f"{label}: editor connection lost mid-call: {msg}"
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def test_list_actors_round_trip():
|
||||
"""A read action proves unwrapping: 'actors' is an INNER field of the action result."""
|
||||
r = run(disp._dispatch("actor", "list_all_with_locations", {}))
|
||||
assert r.get("success") is True
|
||||
assert "actors" in r, f"inner field missing — response not unwrapped: {r}"
|
||||
assert isinstance(r["actors"], list)
|
||||
|
||||
|
||||
def test_spawn_and_delete_round_trip():
|
||||
spawn = run(disp._dispatch("actor", "spawn_from_class",
|
||||
{"class_path": "/Script/Engine.PointLight",
|
||||
"location": [0, 0, 700]}))
|
||||
assert spawn.get("success") is True, spawn
|
||||
label = spawn.get("actor_label")
|
||||
assert label, f"actor_label missing — not unwrapped: {spawn}"
|
||||
# cleanup through the same chain
|
||||
deleted = run(disp._dispatch("actor", "delete_by_label", {"actor_label": label}))
|
||||
assert deleted.get("success") is True, deleted
|
||||
|
||||
|
||||
def test_inner_action_failure_is_surfaced():
|
||||
"""Action failure must come back as success=False (not buried in a result string)."""
|
||||
r = run(disp._dispatch("actor", "set_transform",
|
||||
{"actor_label": "NoSuchActor_E2E_XYZ", "location": [0, 0, 0]}))
|
||||
assert r.get("success") is False
|
||||
assert "message" in r
|
||||
|
||||
|
||||
def test_list_actions_offline_path_still_works():
|
||||
"""list_actions must not touch TCP even when the editor is up."""
|
||||
r = run(disp._dispatch("material", "list_actions", {}))
|
||||
assert r["success"] is True
|
||||
assert r["domain"] == "material"
|
||||
|
||||
|
||||
def test_execute_python_round_trip():
|
||||
"""execute_python runs real Unreal Python through the chain."""
|
||||
r = run(disp.util.fn(action="execute_python",
|
||||
params={"code": "print('e2e_marker_42')"}))
|
||||
# send_python_exec returns the raw wrapper; the printed marker is in 'result'.
|
||||
blob = r.get("result", "") + r.get("message", "")
|
||||
assert "e2e_marker_42" in blob, f"execute_python did not echo marker: {r}"
|
||||
|
||||
|
||||
def test_vision_capture_returns_image():
|
||||
"""vision capture_viewport returns an MCP Image (PNG) through the full chain."""
|
||||
from fastmcp import Image
|
||||
r = run(disp.vision.fn(action="capture_viewport", params={"width": 320, "height": 180}))
|
||||
assert isinstance(r, Image), f"expected an Image, got {type(r).__name__}: {r}"
|
||||
# the PNG bytes should carry a valid signature
|
||||
data = getattr(r, "data", b"")
|
||||
assert data[:4] == b"\x89PNG", "capture did not return a PNG"
|
||||
|
||||
|
||||
def test_gltf_import_round_trip():
|
||||
"""glTF import via the deferred-tick path: export the engine Cube to .glb, import it,
|
||||
then poll get_gltf_import_status until done. Each dispatch is its own game-thread task,
|
||||
so the editor ticks between calls and the Interchange async import can run + complete."""
|
||||
import time
|
||||
|
||||
# 1. export /Engine/BasicShapes/Cube to a temp .glb (via execute_python)
|
||||
export_code = (
|
||||
"import unreal, os\n"
|
||||
"glb = os.path.join(unreal.Paths.project_saved_dir(), 'MCP_E2E_gltf.glb').replace(chr(92), '/')\n"
|
||||
"cube = unreal.EditorAssetLibrary.load_asset('/Engine/BasicShapes/Cube')\n"
|
||||
"t = unreal.AssetExportTask(); t.object = cube; t.filename = glb\n"
|
||||
"t.automated = True; t.replace_identical = True; t.prompt = False\n"
|
||||
"ok = unreal.Exporter.run_asset_export_task(t)\n"
|
||||
"print('GLBPATH=' + glb if (ok and os.path.isfile(glb)) else 'GLBFAIL')\n"
|
||||
)
|
||||
r = run(disp.util.fn(action="execute_python", params={"code": export_code}))
|
||||
blob = (r.get("result", "") or "") + (r.get("message", "") or "")
|
||||
assert "GLBPATH=" in blob, f"glb export failed: {blob[:300]}"
|
||||
glb = blob.split("GLBPATH=", 1)[1].split()[0].strip().strip('"')
|
||||
|
||||
dest = "/Game/Tests/MCP_E2E/glb"
|
||||
try:
|
||||
imp = run(disp._dispatch("asset", "import_gltf",
|
||||
{"file_path": glb, "destination_path": dest}))
|
||||
assert imp.get("success") and imp.get("pending"), f"schedule failed: {imp}"
|
||||
|
||||
st = {}
|
||||
for _ in range(40): # ~20s budget for the async Interchange import
|
||||
st = run(disp._dispatch("asset", "get_gltf_import_status",
|
||||
{"destination_path": dest}))
|
||||
_assert_not_connection_error(st, "get_gltf_import_status")
|
||||
if st.get("done"):
|
||||
break
|
||||
time.sleep(0.5)
|
||||
assert st.get("success") and st.get("done"), f"import did not complete: {st}"
|
||||
classes = [a["class"] for a in st["imported_assets"]]
|
||||
assert "StaticMesh" in classes, f"no StaticMesh among imported: {st}"
|
||||
finally:
|
||||
run(disp.util.fn(action="execute_python", params={
|
||||
"code": f"import unreal; unreal.EditorAssetLibrary.delete_directory('{dest}')"}))
|
||||
|
||||
|
||||
# ── exhaustive: every catalog action survives the full chain ───────────────────
|
||||
|
||||
def _all_action_pairs():
|
||||
pairs = []
|
||||
for domain, actions in CATALOG.items():
|
||||
for action in actions:
|
||||
if (domain, action) in _EXCLUDE:
|
||||
continue
|
||||
pairs.append((domain, action))
|
||||
return pairs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("domain,action", _all_action_pairs())
|
||||
def test_every_action_round_trips(domain, action):
|
||||
"""
|
||||
Drive every action through the real chain with empty params and assert we get
|
||||
back an unwrapped dict containing 'success'. This proves routing + TCP +
|
||||
C++ dispatch + result unwrapping work for the action — independent of whether
|
||||
the action *succeeds* with no args (validation failures still return a dict).
|
||||
|
||||
Empty params are safe: execute_action wraps any exception (incl. missing-arg
|
||||
TypeError) as {"success": false, ...}, and ue_* functions validate required
|
||||
params before doing work.
|
||||
"""
|
||||
if domain == "util":
|
||||
r = run(disp.util.fn(action=action, params={}))
|
||||
elif domain == "vision":
|
||||
r = run(disp.vision.fn(action=action, params={}))
|
||||
else:
|
||||
r = run(disp._dispatch(domain, action, {}))
|
||||
assert isinstance(r, dict), f"{domain}.{action} returned non-dict: {r!r}"
|
||||
assert "success" in r, f"chain/unwrap failed for {domain}.{action}: {r!r}"
|
||||
_assert_not_connection_error(r, f"{domain}.{action}")
|
||||
|
||||
|
||||
def test_zzz_editor_survived_suite():
|
||||
"""Last test in the file: the editor must still be alive after the full sweep."""
|
||||
assert _editor_reachable(), "Unreal editor is no longer reachable after the E2E suite (it crashed mid-run)."
|
||||
590
mcp-server/uv.lock
generated
Normal file
590
mcp-server/uv.lock
generated
Normal file
@@ -0,0 +1,590 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[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, 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, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ 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, 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, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/9d/b1e08d36899c12c8b894a44a5583ee157789f26fc4b176f8e4b6217b56e1/authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210", size = 158371, upload-time = "2025-05-23T00:21:45.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981, upload-time = "2025-05-23T00:21:43.075Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.4.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||
]
|
||||
|
||||
[[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, 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, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "45.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ba/cf442ae99ef363855ed84b39e0fb3c106ac66b7a7703f3c9c9cfe05412cb/cryptography-45.0.4-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4828190fb6c4bcb6ebc6331f01fe66ae838bb3bd58e753b59d4b22eb444b996c", size = 3590512, upload-time = "2025-06-10T00:03:36.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899, upload-time = "2025-06-10T00:03:38.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900, upload-time = "2025-06-10T00:03:40.233Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422, upload-time = "2025-06-10T00:03:41.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475, upload-time = "2025-06-10T00:03:43.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/49/0ab9774f64555a1b50102757811508f5ace451cf5dc0a2d074a4b9deca6a/cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d", size = 3337594, upload-time = "2025-06-10T00:03:45.523Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
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, 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, upload-time = "2025-05-10T17:42:49.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastmcp"
|
||||
version = "2.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "authlib" },
|
||||
{ name = "exceptiongroup" },
|
||||
{ name = "httpx" },
|
||||
{ name = "mcp" },
|
||||
{ name = "openapi-pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/69/8820d3c0e17ed2c7baed3e322191509285fc724c60f9cac5b28037feb5c9/fastmcp-2.7.1.tar.gz", hash = "sha256:489b8480a3e3a96b9eb1847e77f0272b732ad397b2ddad3a25eb185cc99b6c9c", size = 1591616, upload-time = "2025-06-08T01:50:02.349Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/b8/af0bb06d1388b680c64ec7b9767d3718e51e65d91e425c1296446f10a9fc/fastmcp-2.7.1-py3-none-any.whl", hash = "sha256:e75b4c7088338f2532d79f37a2ae654f47bfd7d3d15340233fda25bc168231b6", size = 127618, upload-time = "2025-06-08T01:50:00.945Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ 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, 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, 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, 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, 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, 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 = "iniconfig"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
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, 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, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.9.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/df/8fefc0c6c7a5c66914763e3ff3893f9a03435628f6625d5e3b0dc45d73db/mcp-1.9.3.tar.gz", hash = "sha256:587ba38448e81885e5d1b84055cfcc0ca56d35cd0c58f50941cab01109405388", size = 333045, upload-time = "2025-06-05T15:48:25.681Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/45/823ad05504bea55cb0feb7470387f151252127ad5c72f8882e8fe6cf5c0e/mcp-1.9.3-py3-none-any.whl", hash = "sha256:69b0136d1ac9927402ed4cf221d4b8ff875e7132b0b06edd446448766f34f9b9", size = 131063, upload-time = "2025-06-05T15:48:24.171Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/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 = "openapi-pydantic"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.5"
|
||||
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/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
|
||||
]
|
||||
|
||||
[[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, 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, upload-time = "2025-01-06T17:26:25.553Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
|
||||
]
|
||||
|
||||
[[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 = "rich"
|
||||
version = "14.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
|
||||
]
|
||||
|
||||
[[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, 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, 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, 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, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.47.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/d0/0332bd8a25779a0e2082b0e179805ad39afad642938b371ae0882e7f880d/starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af", size = 2582856, upload-time = "2025-05-29T15:45:27.628Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/81/c60b35fe9674f63b38a8feafc414fca0da378a9dbd5fa1e0b8d23fcc7a9b/starlette-0.47.0-py3-none-any.whl", hash = "sha256:9d052d4933683af40ffd47c7465433570b4949dc937e20ad1d73b34e72f10c37", size = 72796, upload-time = "2025-05-29T15:45:26.305Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unrealmcp"
|
||||
version = "2.2.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastmcp" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
||||
]
|
||||
39
mcp-server/validate_tools.py
Normal file
39
mcp-server/validate_tools.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
||||
|
||||
"""
|
||||
Validates that the dispatcher action catalog is in sync with the plugin's
|
||||
ue_* function signatures.
|
||||
|
||||
The catalog (dispatchers/_catalog.py) is auto-generated by generate_catalog.py.
|
||||
This script re-runs the generator in --check mode: if the committed catalog
|
||||
differs from what the current signatures would produce, it fails.
|
||||
|
||||
This catches the bug class where catalog param names drift from the real
|
||||
ue_* signatures (the dispatcher passes params straight through as
|
||||
ue_func(**params), so a name mismatch = runtime failure).
|
||||
|
||||
Usage:
|
||||
python validate_tools.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
GENERATOR = Path(__file__).parent / "generate_catalog.py"
|
||||
|
||||
|
||||
def main():
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(GENERATOR), "--check"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
sys.stdout.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user