From 225b161e821d435327f9af96eb42ed2fcad65036 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:58:58 -0500 Subject: [PATCH 1/7] Initial update on tool list update --- .claude/mcp.json | 12 +- .claude/settings.json | 6 - .gitignore | 6 +- .../Editor/Services/IToolDiscoveryService.cs | 1 + .../Editor/Services/ToolDiscoveryService.cs | 3 +- .../Transports/WebSocketTransportClient.cs | 3 +- .../Editor/Tools/Animation/ManageAnimation.cs | 2 +- MCPForUnity/Editor/Tools/GetTestJob.cs | 2 +- MCPForUnity/Editor/Tools/ManageScene.cs | 10 + .../Editor/Tools/ManageScriptableObject.cs | 2 +- MCPForUnity/Editor/Tools/ManageShader.cs | 2 +- MCPForUnity/Editor/Tools/ManageTexture.cs | 2 +- MCPForUnity/Editor/Tools/ManageUI.cs | 2 +- .../Editor/Tools/McpForUnityToolAttribute.cs | 9 + MCPForUnity/Editor/Tools/RunTests.cs | 2 +- MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs | 2 +- .../Editor/Windows/Components/Common.uss | 9 +- .../Components/Tools/McpToolsSection.cs | 129 ++++- Server/pyproject.toml | 2 +- Server/src/main.py | 22 +- Server/src/services/custom_tool_service.py | 8 +- Server/src/services/registry/__init__.py | 6 + Server/src/services/registry/tool_registry.py | 52 ++ Server/src/services/resources/active_tool.py | 2 +- Server/src/services/resources/custom_tools.py | 4 +- Server/src/services/resources/editor_state.py | 6 +- Server/src/services/resources/gameobject.py | 6 +- Server/src/services/resources/layers.py | 2 +- Server/src/services/resources/menu_items.py | 2 +- Server/src/services/resources/prefab.py | 4 +- Server/src/services/resources/prefab_stage.py | 2 +- Server/src/services/resources/project_info.py | 2 +- Server/src/services/resources/selection.py | 2 +- Server/src/services/resources/tags.py | 2 +- Server/src/services/resources/tests.py | 4 +- Server/src/services/resources/tool_groups.py | 44 ++ .../src/services/resources/unity_instances.py | 4 +- Server/src/services/resources/windows.py | 2 +- Server/src/services/tools/__init__.py | 26 +- Server/src/services/tools/batch_execute.py | 2 +- .../services/tools/debug_request_context.py | 7 +- .../src/services/tools/execute_custom_tool.py | 5 +- .../src/services/tools/execute_menu_item.py | 2 +- Server/src/services/tools/find_gameobjects.py | 2 +- Server/src/services/tools/find_in_file.py | 2 +- Server/src/services/tools/manage_animation.py | 3 +- Server/src/services/tools/manage_asset.py | 2 +- .../src/services/tools/manage_components.py | 2 +- Server/src/services/tools/manage_editor.py | 2 +- .../src/services/tools/manage_gameobject.py | 2 +- Server/src/services/tools/manage_material.py | 2 +- Server/src/services/tools/manage_prefabs.py | 2 +- Server/src/services/tools/manage_scene.py | 2 +- Server/src/services/tools/manage_script.py | 13 +- .../tools/manage_scriptable_object.py | 3 +- Server/src/services/tools/manage_shader.py | 3 +- Server/src/services/tools/manage_texture.py | 3 +- Server/src/services/tools/manage_tools.py | 112 ++++ Server/src/services/tools/manage_ui.py | 3 +- Server/src/services/tools/manage_vfx.py | 3 +- Server/src/services/tools/read_console.py | 2 +- Server/src/services/tools/refresh_unity.py | 2 +- Server/src/services/tools/run_tests.py | 6 +- .../src/services/tools/script_apply_edits.py | 2 +- .../src/services/tools/set_active_instance.py | 13 +- Server/src/transport/plugin_hub.py | 87 ++- .../transport/unity_instance_middleware.py | 58 +- .../test_debug_request_context_diagnostics.py | 7 +- Server/tests/integration/test_helpers.py | 4 +- .../integration/test_inline_unity_instance.py | 134 +++-- .../integration/test_instance_autoselect.py | 31 +- .../test_instance_routing_comprehensive.py | 98 ++-- .../test_instance_targeting_resolution.py | 8 +- .../test_manage_scriptable_object_tool.py | 30 +- .../test_middleware_auth_integration.py | 18 +- .../test_multi_user_session_isolation.py | 8 +- .../test_refresh_unity_retry_recovery.py | 2 +- .../test_custom_tool_service_user_scope.py | 4 +- Server/tests/test_manage_animation.py | 12 +- Server/tests/test_manage_vfx_actions.py | 3 +- .../tests/test_transport_characterization.py | 89 +-- Server/uv.lock | 530 ++++++------------ 82 files changed, 1026 insertions(+), 707 deletions(-) create mode 100644 Server/src/services/resources/tool_groups.py create mode 100644 Server/src/services/tools/manage_tools.py diff --git a/.claude/mcp.json b/.claude/mcp.json index 63da78661..ae0c7a456 100644 --- a/.claude/mcp.json +++ b/.claude/mcp.json @@ -1,16 +1,8 @@ { "mcpServers": { "UnityMCP": { - "type": "stdio", - "command": "uv", - "args": [ - "run", - "--directory", - "${workspaceFolder}/Server", - "src/main.py", - "--transport", - "stdio" - ] + "type": "http", + "url": "http://localhost:8080/mcp" } } } diff --git a/.claude/settings.json b/.claude/settings.json index 127026519..29f403bcd 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -6,12 +6,6 @@ "MultiEdit(reports/**)" ], "deny": [ - "WebFetch", - "WebSearch", - "Task", - "TodoWrite", - "NotebookEdit", - "NotebookRead" ] } } diff --git a/.gitignore b/.gitignore index 6cd591730..88246fdde 100644 --- a/.gitignore +++ b/.gitignore @@ -55,9 +55,9 @@ TestProjects/UnityMCPTests/Assets/Temp/ # CI test reports (generated during test runs) reports/ -# Local Claude configs (not for repo) -.claude/local/ - # Local testing harness scripts/local-test/ .claude/settings.local.json + +# Ignore the .claude directory, since it might contain local/project-level setting such as deny and allowlist. +/.claude diff --git a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs index ee2c616b7..090b366c7 100644 --- a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs +++ b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs @@ -18,6 +18,7 @@ public class ToolMetadata public bool RequiresPolling { get; set; } = false; public string PollAction { get; set; } = "status"; public bool IsBuiltIn { get; set; } + public string Group { get; set; } = "core"; } /// diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs index ac7cc1455..b7ef5d1eb 100644 --- a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs +++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs @@ -132,7 +132,8 @@ private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute too AssemblyName = type.Assembly.GetName().Name, AutoRegister = toolAttr.AutoRegister, RequiresPolling = toolAttr.RequiresPolling, - PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction + PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction, + Group = toolAttr.Group ?? "core" }; metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType( diff --git a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs index 8832b3227..71106a071 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs @@ -539,7 +539,8 @@ private async Task SendRegisterToolsAsync(CancellationToken token) ["description"] = tool.Description, ["structured_output"] = tool.StructuredOutput, ["requires_polling"] = tool.RequiresPolling, - ["poll_action"] = tool.PollAction + ["poll_action"] = tool.PollAction, + ["group"] = tool.Group ?? "core" }; var paramsArray = new JArray(); diff --git a/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs b/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs index 98f737249..508994009 100644 --- a/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs +++ b/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs @@ -7,7 +7,7 @@ namespace MCPForUnity.Editor.Tools.Animation { - [McpForUnityTool("manage_animation", AutoRegister = false)] + [McpForUnityTool("manage_animation", AutoRegister = false, Group = "animation")] public static class ManageAnimation { private static readonly Dictionary ParamAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/MCPForUnity/Editor/Tools/GetTestJob.cs b/MCPForUnity/Editor/Tools/GetTestJob.cs index 4817ab9bc..ade8811e5 100644 --- a/MCPForUnity/Editor/Tools/GetTestJob.cs +++ b/MCPForUnity/Editor/Tools/GetTestJob.cs @@ -8,7 +8,7 @@ namespace MCPForUnity.Editor.Tools /// /// Poll a previously started async test job by job_id. /// - [McpForUnityTool("get_test_job", AutoRegister = false)] + [McpForUnityTool("get_test_job", AutoRegister = false, Group = "testing")] public static class GetTestJob { public static object HandleCommand(JObject @params) diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index b07a34c3b..ec59533ae 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -240,6 +240,16 @@ public static object ExecuteScreenshot(string fileName = null, int? superSize = return CaptureScreenshot(cmd); } + /// + /// Captures a 6-angle contact-sheet around the scene bounds centre. + /// Public so the tools UI can reuse the same logic. + /// + public static object ExecuteMultiviewScreenshot(int maxResolution = 480) + { + var cmd = new SceneCommand { maxResolution = maxResolution }; + return CaptureSurroundBatch(cmd); + } + private static object CreateScene(string fullPath, string relativePath) { if (File.Exists(fullPath)) diff --git a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs index 62d1c19a5..b5a4d78f3 100644 --- a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs +++ b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs @@ -17,7 +17,7 @@ namespace MCPForUnity.Editor.Tools /// /// Patching is performed via SerializedObject/SerializedProperty paths (Unity-native), not reflection. /// - [McpForUnityTool("manage_scriptable_object", AutoRegister = false)] + [McpForUnityTool("manage_scriptable_object", AutoRegister = false, Group = "scripting_ext")] public static class ManageScriptableObject { private const string CodeCompilingOrReloading = "compiling_or_reloading"; diff --git a/MCPForUnity/Editor/Tools/ManageShader.cs b/MCPForUnity/Editor/Tools/ManageShader.cs index 849a6f932..64cfbeb78 100644 --- a/MCPForUnity/Editor/Tools/ManageShader.cs +++ b/MCPForUnity/Editor/Tools/ManageShader.cs @@ -12,7 +12,7 @@ namespace MCPForUnity.Editor.Tools /// /// Handles CRUD operations for shader files within the Unity project. /// - [McpForUnityTool("manage_shader", AutoRegister = false)] + [McpForUnityTool("manage_shader", AutoRegister = false, Group = "vfx")] public static class ManageShader { /// diff --git a/MCPForUnity/Editor/Tools/ManageTexture.cs b/MCPForUnity/Editor/Tools/ManageTexture.cs index 86e429045..4ab66f720 100644 --- a/MCPForUnity/Editor/Tools/ManageTexture.cs +++ b/MCPForUnity/Editor/Tools/ManageTexture.cs @@ -13,7 +13,7 @@ namespace MCPForUnity.Editor.Tools /// Supports patterns (checkerboard, stripes, dots, grid, brick), /// gradients, noise, and direct pixel manipulation. /// - [McpForUnityTool("manage_texture", AutoRegister = false)] + [McpForUnityTool("manage_texture", AutoRegister = false, Group = "vfx")] public static class ManageTexture { private const int MaxTextureDimension = 1024; diff --git a/MCPForUnity/Editor/Tools/ManageUI.cs b/MCPForUnity/Editor/Tools/ManageUI.cs index c32253f4b..5947eb1f1 100644 --- a/MCPForUnity/Editor/Tools/ManageUI.cs +++ b/MCPForUnity/Editor/Tools/ManageUI.cs @@ -12,7 +12,7 @@ namespace MCPForUnity.Editor.Tools { - [McpForUnityTool("manage_ui", AutoRegister = false)] + [McpForUnityTool("manage_ui", AutoRegister = false, Group = "ui")] public static class ManageUI { private static readonly HashSet ValidExtensions = new(StringComparer.OrdinalIgnoreCase) diff --git a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs index e4db3a4c6..0d73fbb11 100644 --- a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs +++ b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs @@ -30,6 +30,15 @@ public class McpForUnityToolAttribute : Attribute /// public bool AutoRegister { get; set; } = true; + /// + /// Tool group for dynamic visibility on the Python server. + /// Core tools are enabled by default; other groups start hidden and + /// can be activated per-session via the manage_tools meta-tool. + /// Valid groups: core, vfx, animation, ui, scripting_ext, testing, menu. + /// Set to null for server meta-tools that should always be visible. + /// + public string Group { get; set; } = "core"; + /// /// Enables the polling middleware for long-running tools. When true, Unity /// should return a PendingResponse and the Python side will poll using diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs index abbf9225b..3c93e97d2 100644 --- a/MCPForUnity/Editor/Tools/RunTests.cs +++ b/MCPForUnity/Editor/Tools/RunTests.cs @@ -12,7 +12,7 @@ namespace MCPForUnity.Editor.Tools /// Starts a Unity Test Runner run asynchronously and returns a job id immediately. /// Use get_test_job(job_id) to poll status/results. /// - [McpForUnityTool("run_tests", AutoRegister = false)] + [McpForUnityTool("run_tests", AutoRegister = false, Group = "testing")] public static class RunTests { public static Task HandleCommand(JObject @params) diff --git a/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs b/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs index 685bbf4cb..085e4069e 100644 --- a/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs +++ b/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs @@ -117,7 +117,7 @@ namespace MCPForUnity.Editor.Tools.Vfx /// /// For full parameter details, refer to Unity documentation for each component type. /// - [McpForUnityTool("manage_vfx", AutoRegister = false)] + [McpForUnityTool("manage_vfx", AutoRegister = false, Group = "vfx")] public static class ManageVFX { private static readonly Dictionary ParamAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/MCPForUnity/Editor/Windows/Components/Common.uss b/MCPForUnity/Editor/Windows/Components/Common.uss index bf2b3b726..966b11d0d 100644 --- a/MCPForUnity/Editor/Windows/Components/Common.uss +++ b/MCPForUnity/Editor/Windows/Components/Common.uss @@ -378,8 +378,10 @@ padding: 2px 6px; margin-left: 4px; margin-top: 2px; - background-color: rgba(100, 100, 100, 0.25); + background-color: rgba(200, 200, 200, 1); border-radius: 3px; + border-width: 1px; + border-color: rgba(160, 160, 160, 1); color: rgba(40, 40, 40, 1); } @@ -529,8 +531,9 @@ } .unity-theme-dark .tool-tag { - color: rgba(220, 220, 220, 1); - background-color: rgba(80, 80, 80, 0.6); + color: rgba(255, 255, 255, 0.9); + background-color: rgba(90, 95, 105, 1); + border-color: rgba(110, 115, 125, 1); } .unity-theme-dark .tool-item { diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs index 2ab0c5a82..6609cf352 100644 --- a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs @@ -16,6 +16,7 @@ namespace MCPForUnity.Editor.Windows.Components.Tools /// /// Controller for the Tools section inside the MCP For Unity editor window. /// Provides discovery, filtering, and per-tool enablement toggles. + /// Tools are grouped by their Group property (core first, then alphabetical). /// public class McpToolsSection { @@ -29,6 +30,17 @@ public class McpToolsSection private VisualElement categoryContainer; private List allTools = new(); + /// Human-friendly names for tool groups shown in the UI. + private static readonly Dictionary GroupDisplayNames = new(StringComparer.OrdinalIgnoreCase) + { + { "core", "Core Tools" }, + { "vfx", "VFX & Shaders" }, + { "animation", "Animation" }, + { "ui", "UI Toolkit" }, + { "scripting_ext", "Scripting Extensions" }, + { "testing", "Testing" }, + }; + public VisualElement Root { get; } public McpToolsSection(VisualElement root) @@ -92,6 +104,9 @@ private void RegisterCallbacks() /// /// Rebuilds the tool list and synchronises toggle states. + /// Tools are displayed in group-based foldouts: core first, then other + /// groups alphabetically. Custom (non-built-in) tools appear in a + /// separate "Custom Tools" foldout at the bottom. /// public void Refresh() { @@ -100,8 +115,7 @@ public void Refresh() var service = MCPServiceLocator.ToolDiscovery; allTools = service.DiscoverAllTools() - .OrderBy(tool => IsBuiltIn(tool) ? 0 : 1) - .ThenBy(tool => tool.Name, StringComparer.OrdinalIgnoreCase) + .OrderBy(tool => tool.Name, StringComparer.OrdinalIgnoreCase) .ToList(); bool hasTools = allTools.Count > 0; @@ -111,6 +125,13 @@ public void Refresh() if (noteLabel != null) { noteLabel.style.display = hasTools ? DisplayStyle.Flex : DisplayStyle.None; + if (hasTools) + { + bool isHttp = EditorConfigurationCache.Instance.UseHttpTransport; + noteLabel.text = isHttp + ? "Changes apply after reconnecting or re-registering tools." + : "Stdio mode: per-tool toggles only affect HTTP transport. Use the manage_tools meta-tool to change group visibility in stdio sessions."; + } } if (!hasTools) @@ -120,21 +141,46 @@ public void Refresh() return; } - BuildCategory("Built-in Tools", "built-in", allTools.Where(IsBuiltIn)); - + // Partition into built-in and custom + var builtInTools = allTools.Where(IsBuiltIn).ToList(); var customTools = allTools.Where(tool => !IsBuiltIn(tool)).ToList(); - if (customTools.Count > 0) + + // Group built-in tools by their Group property + var grouped = builtInTools + .GroupBy(t => t.Group ?? "core") + .ToDictionary(g => g.Key, g => g.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase).ToList()); + + // Render "core" first, then remaining groups alphabetically + if (grouped.TryGetValue("core", out var coreTools)) { - BuildCategory("Custom Tools", "custom", customTools); + BuildCategory(GetGroupDisplayName("core"), "group-core", coreTools); + grouped.Remove("core"); } - else + + foreach (var kvp in grouped.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase)) { - AddInfoLabel("No custom tools detected in loaded assemblies."); + BuildCategory(GetGroupDisplayName(kvp.Key), $"group-{kvp.Key}", kvp.Value); + } + + // Custom tools at the bottom + if (customTools.Count > 0) + { + BuildCategory("Custom Tools", "custom", customTools); } UpdateSummary(); } + private static string GetGroupDisplayName(string group) + { + if (GroupDisplayNames.TryGetValue(group, out var displayName)) + return displayName; + // Fallback: capitalize first letter + return string.IsNullOrEmpty(group) + ? "Other" + : char.ToUpper(group[0]) + group.Substring(1); + } + private void BuildCategory(string title, string prefsSuffix, IEnumerable tools) { var toolList = tools.ToList(); @@ -143,10 +189,14 @@ private void BuildCategory(string title, string prefsSuffix, IEnumerable MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name)); + + // Default foldout state: core is open, others collapsed + bool defaultOpen = prefsSuffix == "group-core"; var foldout = new Foldout { - text = $"{title} ({toolList.Count})", - value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, true) + text = $"{title} ({enabledCount}/{toolList.Count})", + value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, defaultOpen) }; foldout.RegisterValueChangedCallback(evt => @@ -201,7 +251,9 @@ private VisualElement CreateToolRow(ToolMetadata tool) header.Add(tagsContainer); row.Add(header); - if (!string.IsNullOrWhiteSpace(tool.Description)) + // Skip auto-generated placeholder descriptions like "Tool: find_gameobjects" + if (!string.IsNullOrWhiteSpace(tool.Description) + && !tool.Description.StartsWith("Tool: ", StringComparison.OrdinalIgnoreCase)) { var description = new Label(tool.Description); description.AddToClassList("tool-item-description"); @@ -347,7 +399,21 @@ private VisualElement CreateManageSceneActions() screenshotButton.style.marginTop = 4; screenshotButton.tooltip = "Capture a screenshot to Assets/Screenshots via manage_scene."; - actions.Add(screenshotButton); + var multiviewButton = new Button(OnManageSceneMultiviewClicked) + { + text = "Capture Multiview" + }; + multiviewButton.AddToClassList("tool-action-button"); + multiviewButton.style.marginTop = 4; + multiviewButton.style.marginLeft = 4; + multiviewButton.tooltip = "Capture a 6-angle contact sheet around the scene centre and save to Assets/Screenshots."; + + var row = new VisualElement(); + row.style.flexDirection = FlexDirection.Row; + row.Add(screenshotButton); + row.Add(multiviewButton); + + actions.Add(row); return actions; } @@ -421,6 +487,45 @@ private void OnManageSceneScreenshotClicked() } } + private void OnManageSceneMultiviewClicked() + { + try + { + var response = ManageScene.ExecuteMultiviewScreenshot(); + if (response is SuccessResponse success) + { + // The data object is an anonymous type with imageBase64 — serialize to extract it + var json = Newtonsoft.Json.Linq.JObject.FromObject(success.Data); + string base64 = json["imageBase64"]?.ToString(); + if (!string.IsNullOrEmpty(base64)) + { + string folder = System.IO.Path.Combine(UnityEngine.Application.dataPath, "Screenshots"); + if (!System.IO.Directory.Exists(folder)) + System.IO.Directory.CreateDirectory(folder); + + string fileName = $"Multiview_{System.DateTime.Now:yyyyMMdd_HHmmss}.png"; + string filePath = System.IO.Path.Combine(folder, fileName); + System.IO.File.WriteAllBytes(filePath, Convert.FromBase64String(base64)); + AssetDatabase.Refresh(); + + McpLog.Info($"Multiview contact sheet saved to Assets/Screenshots/{fileName}"); + } + else + { + McpLog.Info(success.Message ?? "Multiview capture completed."); + } + } + else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error)) + { + McpLog.Error(error.Error); + } + } + catch (Exception ex) + { + McpLog.Error($"Failed to capture multiview: {ex.Message}"); + } + } + private static Label CreateTag(string text) { var tag = new Label(text); diff --git a/Server/pyproject.toml b/Server/pyproject.toml index a1e58bf47..e58c6356e 100644 --- a/Server/pyproject.toml +++ b/Server/pyproject.toml @@ -30,7 +30,7 @@ keywords = ["mcp", "unity", "ai", "model context protocol", "gamedev", "unity3d" requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", - "fastmcp==2.14.1", + "fastmcp>=3.0.0,<4", "mcp>=1.16.0", "pydantic>=2.12.5", "tomli>=2.3.0", diff --git a/Server/src/main.py b/Server/src/main.py index 731f7ee0d..a8d3486da 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -162,7 +162,7 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]: if _plugin_registry is None: _plugin_registry = PluginRegistry() loop = asyncio.get_running_loop() - PluginHub.configure(_plugin_registry, loop) + PluginHub.configure(_plugin_registry, loop, mcp=server) # Record server startup telemetry start_time = time.time() @@ -347,6 +347,21 @@ async def health_http(_: Request) -> JSONResponse: "message": "MCP for Unity server is running" }) + @mcp.custom_route("/api/debug/list-tools", methods=["GET"]) + async def debug_list_tools(_: Request) -> JSONResponse: + """Diagnostic endpoint: returns registered tools and transform info (bypasses middleware).""" + try: + # Access the internal tool manager to avoid creating a dummy MCP context + tool_manager = mcp._tool_manager + all_tool_names = sorted(tool_manager.tools.keys()) if tool_manager else [] + return JSONResponse({ + "registered_count": len(all_tool_names), + "tools": all_tool_names, + "transform_count": len(mcp._transforms), + }) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + @mcp.custom_route("/api/auth/login-url", methods=["GET"]) async def auth_login_url(_: Request) -> JSONResponse: """Return the login URL for users to obtain/manage API keys.""" @@ -586,7 +601,10 @@ async def cli_custom_tools_route(request: Request) -> JSONResponse: config.api_key_cache_ttl, ) - # Mount plugin websocket hub at /hub/plugin when HTTP transport is active + # Mount plugin websocket hub at /hub/plugin when HTTP transport is active. + # NOTE: Uses FastMCP private API because custom_route() only supports HTTP + # methods, not WebSocket. _additional_http_routes accepts Starlette Route + # objects and is still present in FastMCP 3.x. existing_routes = [ route for route in mcp._get_additional_http_routes() if isinstance(route, WebSocketRoute) and route.path == "/hub/plugin" diff --git a/Server/src/services/custom_tool_service.py b/Server/src/services/custom_tool_service.py index 91349750c..af9a4c776 100644 --- a/Server/src/services/custom_tool_service.py +++ b/Server/src/services/custom_tool_service.py @@ -29,7 +29,7 @@ _MAX_POLL_SECONDS = 600 -def get_user_id_from_context(ctx: Context) -> str | None: +async def get_user_id_from_context(ctx: Context) -> str | None: """Read user_id from request-scoped context in remote-hosted mode.""" if not config.http_remote_hosted: return None @@ -39,7 +39,7 @@ def get_user_id_from_context(ctx: Context) -> str | None: return None try: - user_id = get_state("user_id") + user_id = await get_state("user_id") except Exception: return None @@ -367,7 +367,7 @@ def _register_global_tool(self, definition: ToolDefinitionModel) -> None: def _build_global_tool_handler(self, definition: ToolDefinitionModel): async def _handler(ctx: Context, **kwargs) -> MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) if not unity_instance: return MCPResponse( success=False, @@ -382,7 +382,7 @@ async def _handler(ctx: Context, **kwargs) -> MCPResponse: ) params = {k: v for k, v in kwargs.items() if v is not None} - user_id = get_user_id_from_context(ctx) + user_id = await get_user_id_from_context(ctx) service = CustomToolService.get_instance() return await service.execute_tool( project_id, diff --git a/Server/src/services/registry/__init__.py b/Server/src/services/registry/__init__.py index 179da96d5..bf14799b1 100644 --- a/Server/src/services/registry/__init__.py +++ b/Server/src/services/registry/__init__.py @@ -4,7 +4,10 @@ from .tool_registry import ( mcp_for_unity_tool, get_registered_tools, + get_group_tool_names, clear_tool_registry, + TOOL_GROUPS, + DEFAULT_ENABLED_GROUPS, ) from .resource_registry import ( mcp_for_unity_resource, @@ -15,7 +18,10 @@ __all__ = [ 'mcp_for_unity_tool', 'get_registered_tools', + 'get_group_tool_names', 'clear_tool_registry', + 'TOOL_GROUPS', + 'DEFAULT_ENABLED_GROUPS', 'mcp_for_unity_resource', 'get_registered_resources', 'clear_resource_registry' diff --git a/Server/src/services/registry/tool_registry.py b/Server/src/services/registry/tool_registry.py index 732cde2c3..346fa7ebe 100644 --- a/Server/src/services/registry/tool_registry.py +++ b/Server/src/services/registry/tool_registry.py @@ -1,16 +1,37 @@ """ Tool registry for auto-discovery of MCP tools. + +Tools can be assigned to *groups* via the ``group`` parameter. Groups map to +FastMCP tags (``"group:"``) which drive the per-session visibility +system exposed through the ``manage_tools`` meta-tool. + +The special group value ``None`` means the tool is *always visible* and +cannot be disabled by the group system (used for server meta-tools like +``set_active_instance`` and ``manage_tools``). """ from typing import Callable, Any # Global registry to collect decorated tools _tool_registry: list[dict[str, Any]] = [] +# Valid group names. ``None`` is also accepted (always-visible meta-tools). +TOOL_GROUPS: dict[str, str] = { + "core": "Essential scene, script, asset & editor tools (always on by default)", + "vfx": "Visual effects – VFX Graph, shaders, procedural textures", + "animation": "Animator control & AnimationClip creation", + "ui": "UI Toolkit (UXML, USS, UIDocument)", + "scripting_ext": "ScriptableObject management", + "testing": "Test runner & async test jobs", +} + +DEFAULT_ENABLED_GROUPS: set[str] = {"core"} + def mcp_for_unity_tool( name: str | None = None, description: str | None = None, unity_target: str | None = "self", + group: str | None = "core", **kwargs ) -> Callable: """ @@ -25,6 +46,10 @@ def mcp_for_unity_tool( - "self" (default): tool follows its own enabled state. - None: server-only tool, always visible in tool listing. - "": alias tool that follows another Unity tool state. + group: Tool group for dynamic visibility. + - A group name string (e.g. "core", "vfx") assigns the tool to + that group and adds a ``tags={"group:"}`` entry. + - None: the tool is *always visible* (server meta-tools). **kwargs: Additional arguments passed to @mcp.tool() Example: @@ -38,6 +63,22 @@ def decorator(func: Callable) -> Callable: tool_kwargs = dict(kwargs) # Create a copy to avoid side effects if "unity_target" in tool_kwargs: del tool_kwargs["unity_target"] + if "group" in tool_kwargs: + del tool_kwargs["group"] + + # Validate and normalize group + resolved_group: str | None = None + if group is not None: + if group not in TOOL_GROUPS: + raise ValueError( + f"Unknown group '{group}' for tool '{tool_name}'. " + f"Valid groups: {', '.join(sorted(TOOL_GROUPS))}." + ) + resolved_group = group + # Merge the group tag into any existing tags the caller provided + existing_tags: set[str] = set(tool_kwargs.get("tags") or set()) + existing_tags.add(f"group:{group}") + tool_kwargs["tags"] = existing_tags if unity_target is None: normalized_unity_target: str | None = None @@ -56,6 +97,7 @@ def decorator(func: Callable) -> Callable: 'name': tool_name, 'description': description, 'unity_target': normalized_unity_target, + 'group': resolved_group, 'kwargs': tool_kwargs, }) @@ -69,6 +111,16 @@ def get_registered_tools() -> list[dict[str, Any]]: return _tool_registry.copy() +def get_group_tool_names() -> dict[str, list[str]]: + """Return a mapping of group name -> list of tool names in that group.""" + result: dict[str, list[str]] = {g: [] for g in TOOL_GROUPS} + for tool in _tool_registry: + g = tool.get("group") + if g and g in result: + result[g].append(tool["name"]) + return result + + def clear_tool_registry(): """Clear the tool registry (useful for testing)""" _tool_registry.clear() diff --git a/Server/src/services/resources/active_tool.py b/Server/src/services/resources/active_tool.py index 2d40becfa..5e5e0aa25 100644 --- a/Server/src/services/resources/active_tool.py +++ b/Server/src/services/resources/active_tool.py @@ -38,7 +38,7 @@ class ActiveToolResponse(MCPResponse): ) async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse: """Get active editor tool information.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/resources/custom_tools.py b/Server/src/services/resources/custom_tools.py index e0fe9c6b1..aacda462f 100644 --- a/Server/src/services/resources/custom_tools.py +++ b/Server/src/services/resources/custom_tools.py @@ -28,7 +28,7 @@ class CustomToolsResourceResponse(MCPResponse): description="Lists custom tools available for the active Unity project.\n\nURI: mcpforunity://custom-tools", ) async def get_custom_tools(ctx: Context) -> CustomToolsResourceResponse | MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) if not unity_instance: return MCPResponse( success=False, @@ -43,7 +43,7 @@ async def get_custom_tools(ctx: Context) -> CustomToolsResourceResponse | MCPRes ) service = CustomToolService.get_instance() - user_id = get_user_id_from_context(ctx) + user_id = await get_user_id_from_context(ctx) tools = await service.list_registered_tools(project_id, user_id=user_id) data = CustomToolsData( diff --git a/Server/src/services/resources/editor_state.py b/Server/src/services/resources/editor_state.py index f14146fac..26d5b36c7 100644 --- a/Server/src/services/resources/editor_state.py +++ b/Server/src/services/resources/editor_state.py @@ -145,8 +145,8 @@ async def infer_single_instance_id(ctx: Context) -> str | None: # HTTP/WebSocket transport: derive from PluginHub sessions. try: # In remote-hosted mode, filter sessions by user_id - user_id = ctx.get_state( - "user_id") if config.http_remote_hosted else None + user_id = (await ctx.get_state( + "user_id")) if config.http_remote_hosted else None sessions_data = await PluginHub.get_sessions(user_id=user_id) sessions = sessions_data.sessions if hasattr( sessions_data, "sessions") else {} @@ -222,7 +222,7 @@ def _enrich_advice_and_staleness(state_v2: dict[str, Any]) -> dict[str, Any]: description="Canonical editor readiness snapshot. Includes advice and server-computed staleness.\n\nURI: mcpforunity://editor/state", ) async def get_editor_state(ctx: Context) -> MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await unity_transport.send_with_unity_instance( async_send_command_with_retry, diff --git a/Server/src/services/resources/gameobject.py b/Server/src/services/resources/gameobject.py index 6097426c5..d4e1a2537 100644 --- a/Server/src/services/resources/gameobject.py +++ b/Server/src/services/resources/gameobject.py @@ -133,7 +133,7 @@ class GameObjectResponse(MCPResponse): ) async def get_gameobject(ctx: Context, instance_id: str) -> MCPResponse: """Get GameObject data by instance ID.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) id_int, error = _validate_instance_id(instance_id) if error: @@ -180,7 +180,7 @@ async def get_gameobject_components( include_properties: bool = True ) -> MCPResponse: """Get all components on a GameObject.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) id_int, error = _validate_instance_id(instance_id) if error: @@ -224,7 +224,7 @@ async def get_gameobject_component( component_name: str ) -> MCPResponse: """Get a specific component on a GameObject.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) id_int, error = _validate_instance_id(instance_id) if error: diff --git a/Server/src/services/resources/layers.py b/Server/src/services/resources/layers.py index f0a4aada1..5b6f18918 100644 --- a/Server/src/services/resources/layers.py +++ b/Server/src/services/resources/layers.py @@ -20,7 +20,7 @@ class LayersResponse(MCPResponse): ) async def get_layers(ctx: Context) -> LayersResponse | MCPResponse: """Get all project layers with their indices.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/resources/menu_items.py b/Server/src/services/resources/menu_items.py index f26ea30ac..c64cb0e0e 100644 --- a/Server/src/services/resources/menu_items.py +++ b/Server/src/services/resources/menu_items.py @@ -20,7 +20,7 @@ class GetMenuItemsResponse(MCPResponse): async def get_menu_items(ctx: Context) -> GetMenuItemsResponse | MCPResponse: """Provides a list of all menu items. """ - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) params = { "refresh": True, "search": "", diff --git a/Server/src/services/resources/prefab.py b/Server/src/services/resources/prefab.py index 73db33d90..b2a676c82 100644 --- a/Server/src/services/resources/prefab.py +++ b/Server/src/services/resources/prefab.py @@ -121,7 +121,7 @@ class PrefabInfoResponse(MCPResponse): ) async def get_prefab_info(ctx: Context, encoded_path: str) -> MCPResponse: """Get prefab asset info by path.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Decode the URL-encoded path decoded_path = _decode_prefab_path(encoded_path) @@ -173,7 +173,7 @@ class PrefabHierarchyResponse(MCPResponse): ) async def get_prefab_hierarchy(ctx: Context, encoded_path: str) -> MCPResponse: """Get prefab hierarchy by path.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Decode the URL-encoded path decoded_path = _decode_prefab_path(encoded_path) diff --git a/Server/src/services/resources/prefab_stage.py b/Server/src/services/resources/prefab_stage.py index 4f3ad3a6d..230882ec9 100644 --- a/Server/src/services/resources/prefab_stage.py +++ b/Server/src/services/resources/prefab_stage.py @@ -30,7 +30,7 @@ class PrefabStageResponse(MCPResponse): ) async def get_prefab_stage(ctx: Context) -> PrefabStageResponse | MCPResponse: """Get current prefab stage information.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/resources/project_info.py b/Server/src/services/resources/project_info.py index 19e45a3cd..816154a8a 100644 --- a/Server/src/services/resources/project_info.py +++ b/Server/src/services/resources/project_info.py @@ -30,7 +30,7 @@ class ProjectInfoResponse(MCPResponse): ) async def get_project_info(ctx: Context) -> ProjectInfoResponse | MCPResponse: """Get static project configuration information.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/resources/selection.py b/Server/src/services/resources/selection.py index 1477768c0..b6499aa42 100644 --- a/Server/src/services/resources/selection.py +++ b/Server/src/services/resources/selection.py @@ -46,7 +46,7 @@ class SelectionResponse(MCPResponse): ) async def get_selection(ctx: Context) -> SelectionResponse | MCPResponse: """Get detailed editor selection information.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/resources/tags.py b/Server/src/services/resources/tags.py index 10c59fc57..54cc4ee44 100644 --- a/Server/src/services/resources/tags.py +++ b/Server/src/services/resources/tags.py @@ -21,7 +21,7 @@ class TagsResponse(MCPResponse): ) async def get_tags(ctx: Context) -> TagsResponse | MCPResponse: """Get all project tags.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/resources/tests.py b/Server/src/services/resources/tests.py index 263e992f2..db127bb1c 100644 --- a/Server/src/services/resources/tests.py +++ b/Server/src/services/resources/tests.py @@ -46,7 +46,7 @@ async def get_tests(ctx: Context) -> GetTestsResponse | MCPResponse: For advanced filtering or pagination control, use the run_tests tool which accepts mode, filter, page_size, and cursor parameters. """ - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, @@ -77,7 +77,7 @@ async def get_tests_for_mode( Returns the first page of tests using Unity's default pagination (50 items). For advanced filtering or pagination control, use the run_tests tool. """ - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, diff --git a/Server/src/services/resources/tool_groups.py b/Server/src/services/resources/tool_groups.py new file mode 100644 index 000000000..669973d60 --- /dev/null +++ b/Server/src/services/resources/tool_groups.py @@ -0,0 +1,44 @@ +""" +tool_groups resource – exposes available tool groups and their metadata. + +URI: mcpforunity://tool-groups +""" +from typing import Any + +from fastmcp import Context + +from services.registry import ( + mcp_for_unity_resource, + TOOL_GROUPS, + DEFAULT_ENABLED_GROUPS, + get_group_tool_names, +) + + +@mcp_for_unity_resource( + uri="mcpforunity://tool-groups", + name="tool_groups", + description=( + "Available tool groups and their tools. " + "Use manage_tools to activate/deactivate groups per session.\n\n" + "URI: mcpforunity://tool-groups" + ), +) +async def get_tool_groups(ctx: Context) -> dict[str, Any]: + group_tools = get_group_tool_names() + groups = [] + for name in sorted(TOOL_GROUPS.keys()): + tools = group_tools.get(name, []) + groups.append({ + "name": name, + "description": TOOL_GROUPS[name], + "default_enabled": name in DEFAULT_ENABLED_GROUPS, + "tools": tools, + "tool_count": len(tools), + }) + return { + "groups": groups, + "total_groups": len(groups), + "default_enabled": sorted(DEFAULT_ENABLED_GROUPS), + "usage": "Call manage_tools(action='activate', group='') to enable a group.", + } diff --git a/Server/src/services/resources/unity_instances.py b/Server/src/services/resources/unity_instances.py index 572efb0be..42ef4af5f 100644 --- a/Server/src/services/resources/unity_instances.py +++ b/Server/src/services/resources/unity_instances.py @@ -40,8 +40,8 @@ async def unity_instances(ctx: Context) -> dict[str, Any]: if transport == "http": # HTTP/WebSocket transport: query PluginHub # In remote-hosted mode, filter sessions by user_id - user_id = ctx.get_state( - "user_id") if config.http_remote_hosted else None + user_id = (await ctx.get_state( + "user_id")) if config.http_remote_hosted else None sessions_data = await PluginHub.get_sessions(user_id=user_id) sessions = sessions_data.sessions diff --git a/Server/src/services/resources/windows.py b/Server/src/services/resources/windows.py index 0e5cbc713..dee9fa96c 100644 --- a/Server/src/services/resources/windows.py +++ b/Server/src/services/resources/windows.py @@ -38,7 +38,7 @@ class WindowsResponse(MCPResponse): ) async def get_windows(ctx: Context) -> WindowsResponse | MCPResponse: """Get all open editor windows.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) response = await send_with_unity_instance( async_send_command_with_retry, unity_instance, diff --git a/Server/src/services/tools/__init__.py b/Server/src/services/tools/__init__.py index 91c1f9e00..8c452c6dd 100644 --- a/Server/src/services/tools/__init__.py +++ b/Server/src/services/tools/__init__.py @@ -9,7 +9,7 @@ from core.telemetry_decorator import telemetry_tool from core.logging_decorator import log_execution from utils.module_discovery import discover_modules -from services.registry import get_registered_tools +from services.registry import get_registered_tools, TOOL_GROUPS, DEFAULT_ENABLED_GROUPS logger = logging.getLogger("mcp-for-unity-server") @@ -26,6 +26,10 @@ def register_all_tools(mcp: FastMCP, *, project_scoped_tools: bool = True): Any .py file in this directory or subdirectories with @mcp_for_unity_tool decorated functions will be automatically registered. + + After registration, non-default tool groups are disabled at the server level + so that new sessions only see the *core* tools (plus always-visible meta-tools). + Clients can activate additional groups at any time via ``manage_tools``. """ logger.info("Auto-discovering MCP for Unity Server tools...") # Dynamic import of all modules in this directory @@ -63,8 +67,22 @@ def register_all_tools(mcp: FastMCP, *, project_scoped_tools: bool = True): logger.info(f"Registered {len(tools)} MCP tools") - -def get_unity_instance_from_context( + # Disable non-default groups at the server level so new sessions start lean. + # Tools with group=None (no tag) are unaffected and always visible. + groups_to_disable = set(TOOL_GROUPS.keys()) - DEFAULT_ENABLED_GROUPS + for group_name in sorted(groups_to_disable): + tag = f"group:{group_name}" + mcp.disable(tags={tag}, components={"tool"}) + logger.debug(f"Disabled tool group at startup: {group_name}") + logger.info( + f"Default tool groups: {', '.join(sorted(DEFAULT_ENABLED_GROUPS))}. " + f"Disabled: {', '.join(sorted(groups_to_disable))}. " + f"Transform count: {len(mcp._transforms)}. " + "Use manage_tools to activate more." + ) + + +async def get_unity_instance_from_context( ctx: Context, key: str = "unity_instance", ) -> str | None: @@ -76,7 +94,7 @@ def get_unity_instance_from_context( get_state_fn = getattr(ctx, "get_state", None) if callable(get_state_fn): try: - return get_state_fn(key) + return await get_state_fn(key) except Exception: # pragma: no cover - defensive pass diff --git a/Server/src/services/tools/batch_execute.py b/Server/src/services/tools/batch_execute.py index 3849dae2a..2c717d9c4 100644 --- a/Server/src/services/tools/batch_execute.py +++ b/Server/src/services/tools/batch_execute.py @@ -85,7 +85,7 @@ async def batch_execute( "Hint for the maximum number of parallel workers"] = None, ) -> dict[str, Any]: """Proxy the batch_execute tool to the Unity Editor transporter.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) if not isinstance(commands, list) or not commands: raise ValueError( diff --git a/Server/src/services/tools/debug_request_context.py b/Server/src/services/tools/debug_request_context.py index 6ce2913fa..ddef0fd1b 100644 --- a/Server/src/services/tools/debug_request_context.py +++ b/Server/src/services/tools/debug_request_context.py @@ -14,13 +14,14 @@ @mcp_for_unity_tool( unity_target=None, + group=None, description="Return the current FastMCP request context details (client_id, session_id, and meta dump).", annotations=ToolAnnotations( title="Debug Request Context", readOnlyHint=True, ), ) -def debug_request_context(ctx: Context) -> dict[str, Any]: +async def debug_request_context(ctx: Context) -> dict[str, Any]: # Check request_context properties rc = getattr(ctx, "request_context", None) rc_client_id = getattr(rc, "client_id", None) @@ -47,8 +48,8 @@ def debug_request_context(ctx: Context) -> dict[str, Any]: # Get session state info via middleware middleware = get_unity_instance_middleware() - derived_key = middleware.get_session_key(ctx) - active_instance = middleware.get_active_instance(ctx) + derived_key = await middleware.get_session_key(ctx) + active_instance = await middleware.get_active_instance(ctx) # Debugging middleware internals # NOTE: These fields expose internal implementation details and may change between versions. diff --git a/Server/src/services/tools/execute_custom_tool.py b/Server/src/services/tools/execute_custom_tool.py index 6152255de..60c1fff89 100644 --- a/Server/src/services/tools/execute_custom_tool.py +++ b/Server/src/services/tools/execute_custom_tool.py @@ -14,6 +14,7 @@ @mcp_for_unity_tool( name="execute_custom_tool", unity_target=None, + group=None, description="Execute a project-scoped custom tool registered by Unity.", annotations=ToolAnnotations( title="Execute Custom Tool", @@ -21,7 +22,7 @@ ), ) async def execute_custom_tool(ctx: Context, tool_name: str, parameters: dict | None = None) -> MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) if not unity_instance: return MCPResponse( success=False, @@ -42,7 +43,7 @@ async def execute_custom_tool(ctx: Context, tool_name: str, parameters: dict | N ) service = CustomToolService.get_instance() - user_id = get_user_id_from_context(ctx) + user_id = await get_user_id_from_context(ctx) return await service.execute_tool( project_id, tool_name, diff --git a/Server/src/services/tools/execute_menu_item.py b/Server/src/services/tools/execute_menu_item.py index 4a2c07be7..af6d46b86 100644 --- a/Server/src/services/tools/execute_menu_item.py +++ b/Server/src/services/tools/execute_menu_item.py @@ -25,7 +25,7 @@ async def execute_menu_item( menu_path: Annotated[str, "Menu path for 'execute' or 'exists' (e.g., 'File/Save Project')"] | None = None, ) -> MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) params_dict: dict[str, Any] = {"menuPath": menu_path} params_dict = {k: v for k, v in params_dict.items() if v is not None} result = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "execute_menu_item", params_dict) diff --git a/Server/src/services/tools/find_gameobjects.py b/Server/src/services/tools/find_gameobjects.py index 2f98801e6..d1e9af015 100644 --- a/Server/src/services/tools/find_gameobjects.py +++ b/Server/src/services/tools/find_gameobjects.py @@ -69,7 +69,7 @@ async def find_gameobjects( - mcpforunity://scene/gameobject/{id}/components - Get all components - mcpforunity://scene/gameobject/{id}/component/{name} - Get specific component """ - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Validate required parameters before preflight I/O if not search_term: diff --git a/Server/src/services/tools/find_in_file.py b/Server/src/services/tools/find_in_file.py index 7a33d95b9..97d0ef1db 100644 --- a/Server/src/services/tools/find_in_file.py +++ b/Server/src/services/tools/find_in_file.py @@ -83,7 +83,7 @@ async def find_in_file( "Case insensitive search"] = True, ) -> dict[str, Any]: # project_root is currently unused but kept for interface consistency - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing find_in_file: {uri} (unity_instance={unity_instance or 'default'})") diff --git a/Server/src/services/tools/manage_animation.py b/Server/src/services/tools/manage_animation.py index 6e4d20f7a..7a66a247f 100644 --- a/Server/src/services/tools/manage_animation.py +++ b/Server/src/services/tools/manage_animation.py @@ -32,6 +32,7 @@ @mcp_for_unity_tool( + group="animation", description=( "Manage Unity animation: Animator control and AnimationClip creation. " "Action prefixes: animator_* (play, crossfade, set parameters, get info), " @@ -86,7 +87,7 @@ async def manage_animation( ), } - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) params_dict: dict[str, Any] = {"action": action_normalized} if properties is not None: diff --git a/Server/src/services/tools/manage_asset.py b/Server/src/services/tools/manage_asset.py index e5328dfa3..248a624ef 100644 --- a/Server/src/services/tools/manage_asset.py +++ b/Server/src/services/tools/manage_asset.py @@ -51,7 +51,7 @@ async def manage_asset( page_number: Annotated[int | float | str, "Page number for pagination (1-based)."] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Best-effort guard: if Unity is compiling/reloading or known external changes are pending, # wait/refresh to avoid stale reads and flaky timeouts. diff --git a/Server/src/services/tools/manage_components.py b/Server/src/services/tools/manage_components.py index 4f26c75ff..d6a5b4ad3 100644 --- a/Server/src/services/tools/manage_components.py +++ b/Server/src/services/tools/manage_components.py @@ -65,7 +65,7 @@ async def manage_components( - Set single property: action="set_property", target="Enemy", component_type="Rigidbody", property="mass", value=5.0 - Set multiple properties: action="set_property", target="Enemy", component_type="Rigidbody", properties={"mass": 5.0, "useGravity": false} """ - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) if gate is not None: diff --git a/Server/src/services/tools/manage_editor.py b/Server/src/services/tools/manage_editor.py index 51480b75d..845d57b56 100644 --- a/Server/src/services/tools/manage_editor.py +++ b/Server/src/services/tools/manage_editor.py @@ -30,7 +30,7 @@ async def manage_editor( "Layer name when adding and removing layers"] | None = None, ) -> dict[str, Any]: # Get active instance from request state (injected by middleware) - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) wait_for_completion = coerce_bool(wait_for_completion) diff --git a/Server/src/services/tools/manage_gameobject.py b/Server/src/services/tools/manage_gameobject.py index 9664d1d23..e96baa940 100644 --- a/Server/src/services/tools/manage_gameobject.py +++ b/Server/src/services/tools/manage_gameobject.py @@ -117,7 +117,7 @@ async def manage_gameobject( ) -> dict[str, Any]: # Get active instance from session state # Removed session_state import - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) if gate is not None: diff --git a/Server/src/services/tools/manage_material.py b/Server/src/services/tools/manage_material.py index 05f487ba6..875eba326 100644 --- a/Server/src/services/tools/manage_material.py +++ b/Server/src/services/tools/manage_material.py @@ -62,7 +62,7 @@ async def manage_material( "Assignment/modification mode"] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # --- Normalize color with validation --- color, color_error = normalize_color(color, output_range="float") diff --git a/Server/src/services/tools/manage_prefabs.py b/Server/src/services/tools/manage_prefabs.py index ac9866202..b737a24b7 100644 --- a/Server/src/services/tools/manage_prefabs.py +++ b/Server/src/services/tools/manage_prefabs.py @@ -85,7 +85,7 @@ async def manage_prefabs( "message": f"Action '{action}' requires parameter '{param_name}'." } - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Preflight check for operations to ensure Unity is ready try: diff --git a/Server/src/services/tools/manage_scene.py b/Server/src/services/tools/manage_scene.py index bb262bb09..479343867 100644 --- a/Server/src/services/tools/manage_scene.py +++ b/Server/src/services/tools/manage_scene.py @@ -149,7 +149,7 @@ async def manage_scene( include_transform: Annotated[bool | str, "If true, include local transform in node summaries."] | None = None, ) -> dict[str, Any] | ToolResult: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) if gate is not None: return gate.model_dump() diff --git a/Server/src/services/tools/manage_script.py b/Server/src/services/tools/manage_script.py index 8da38d2dd..648174504 100644 --- a/Server/src/services/tools/manage_script.py +++ b/Server/src/services/tools/manage_script.py @@ -100,7 +100,7 @@ async def apply_text_edits( options: Annotated[dict[str, Any], "Optional options, used to pass additional options to the script editor"] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing apply_text_edits: {uri} (unity_instance={unity_instance or 'default'})") name, directory = _split_uri(uri) @@ -396,7 +396,7 @@ async def create_script( script_type: Annotated[str, "Script type (e.g., 'C#')"] | None = None, namespace: Annotated[str, "Namespace for the script"] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing create_script: {path} (unity_instance={unity_instance or 'default'})") name = os.path.splitext(os.path.basename(path))[0] @@ -452,7 +452,7 @@ async def delete_script( uri: Annotated[str, "URI of the script to delete under Assets/ directory, mcpforunity://path/Assets/... or file://... or Assets/..."], ) -> dict[str, Any]: """Delete a C# script by URI.""" - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing delete_script: {uri} (unity_instance={unity_instance or 'default'})") name, directory = _split_uri(uri) @@ -490,7 +490,7 @@ async def validate_script( include_diagnostics: Annotated[bool, "Include full diagnostics and summary"] = False, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing validate_script: {uri} (unity_instance={unity_instance or 'default'})") name, directory = _split_uri(uri) @@ -540,7 +540,7 @@ async def manage_script( "Type hint (e.g., 'MonoBehaviour')"] | None = None, namespace: Annotated[str, "Namespace for the script"] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing manage_script: {action} (unity_instance={unity_instance or 'default'})") try: @@ -614,6 +614,7 @@ async def _verify_mutation(): @mcp_for_unity_tool( unity_target=None, + group=None, description=( """Get manage_script capabilities (supported ops, limits, and guards). Returns: @@ -663,7 +664,7 @@ async def get_sha( ctx: Context, uri: Annotated[str, "URI of the script to edit under Assets/ directory, mcpforunity://path/Assets/... or file://... or Assets/..."], ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing get_sha: {uri} (unity_instance={unity_instance or 'default'})") try: diff --git a/Server/src/services/tools/manage_scriptable_object.py b/Server/src/services/tools/manage_scriptable_object.py index a9a51f782..19291cf7a 100644 --- a/Server/src/services/tools/manage_scriptable_object.py +++ b/Server/src/services/tools/manage_scriptable_object.py @@ -23,6 +23,7 @@ @mcp_for_unity_tool( + group="scripting_ext", description="Creates and modifies ScriptableObject assets using Unity SerializedObject property paths.", annotations=ToolAnnotations( title="Manage Scriptable Object", @@ -51,7 +52,7 @@ async def manage_scriptable_object( dry_run: Annotated[bool | str | None, "If true, validate patches without applying (modify only)."] = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Tolerate JSON-string payloads (LLMs sometimes stringify complex objects) parsed_target = parse_json_payload(target) diff --git a/Server/src/services/tools/manage_shader.py b/Server/src/services/tools/manage_shader.py index e8188bb98..96922022b 100644 --- a/Server/src/services/tools/manage_shader.py +++ b/Server/src/services/tools/manage_shader.py @@ -11,6 +11,7 @@ @mcp_for_unity_tool( + group="vfx", description="Manages shader scripts in Unity (create, read, update, delete). Read-only action: read. Modifying actions: create, update, delete.", annotations=ToolAnnotations( title="Manage Shader", @@ -28,7 +29,7 @@ async def manage_shader( ) -> dict[str, Any]: # Get active instance from session state # Removed session_state import - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) try: # Prepare parameters for Unity params = { diff --git a/Server/src/services/tools/manage_texture.py b/Server/src/services/tools/manage_texture.py index fe5780d51..e8eea2fd4 100644 --- a/Server/src/services/tools/manage_texture.py +++ b/Server/src/services/tools/manage_texture.py @@ -374,6 +374,7 @@ def _normalize_import_settings(value: Any) -> tuple[dict | None, str | None]: @mcp_for_unity_tool( + group="vfx", description=( "Procedural texture generation for Unity. Creates textures with solid fills, " "patterns (checkerboard, stripes, dots, grid, brick), gradients, and noise. " @@ -459,7 +460,7 @@ async def manage_texture( "sprite_mesh_type (full_rect/tight), sprite_extrude (0-32)"] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Preflight check gate = await preflight(ctx, wait_for_no_compile=True, refresh_if_dirty=True) diff --git a/Server/src/services/tools/manage_tools.py b/Server/src/services/tools/manage_tools.py new file mode 100644 index 000000000..2eaeb071a --- /dev/null +++ b/Server/src/services/tools/manage_tools.py @@ -0,0 +1,112 @@ +""" +manage_tools – server-only meta-tool for dynamic tool group activation. + +This tool lets the AI assistant (or user) discover available tool groups +and selectively enable / disable them for the current session. Activating +a group makes its tools appear in tool listings; deactivating hides them. + +Works on all transports (stdio, HTTP, SSE) via FastMCP 3.x native +per-session visibility. +""" +from typing import Annotated, Any, Literal + +from fastmcp import Context +from mcp.types import ToolAnnotations + +from services.registry import ( + mcp_for_unity_tool, + TOOL_GROUPS, + DEFAULT_ENABLED_GROUPS, + get_group_tool_names, +) + + +@mcp_for_unity_tool( + unity_target=None, + group=None, + description=( + "Manage which tool groups are visible in this session. " + "Actions: list_groups (show all groups and their status), " + "activate (enable a group), deactivate (disable a group), " + "reset (restore defaults). " + "Activating a group makes its tools appear; deactivating hides them." + ), + annotations=ToolAnnotations( + title="Manage Tools", + readOnlyHint=False, + ), +) +async def manage_tools( + ctx: Context, + action: Annotated[ + Literal["list_groups", "activate", "deactivate", "reset"], + "Action to perform." + ], + group: Annotated[ + str | None, + "Group name (required for activate / deactivate). " + "Valid groups: " + ", ".join(sorted(TOOL_GROUPS.keys())) + ] = None, +) -> dict[str, Any]: + if action == "list_groups": + return _list_groups() + + if action == "activate": + if not group: + return {"error": "group is required for activate"} + if group not in TOOL_GROUPS: + return {"error": f"Unknown group '{group}'. Valid: {', '.join(sorted(TOOL_GROUPS))}"} + tag = f"group:{group}" + await ctx.info(f"Activating tool group: {group}") + await ctx.enable_components(tags={tag}, components={"tool"}) + return { + "activated": group, + "tools": get_group_tool_names().get(group, []), + "message": f"Group '{group}' is now visible. Its tools will appear in tool listings.", + } + + if action == "deactivate": + if not group: + return {"error": "group is required for deactivate"} + if group not in TOOL_GROUPS: + return {"error": f"Unknown group '{group}'. Valid: {', '.join(sorted(TOOL_GROUPS))}"} + tag = f"group:{group}" + await ctx.info(f"Deactivating tool group: {group}") + await ctx.disable_components(tags={tag}, components={"tool"}) + return { + "deactivated": group, + "tools": get_group_tool_names().get(group, []), + "message": f"Group '{group}' is now hidden.", + } + + if action == "reset": + await ctx.info("Resetting tool visibility to defaults") + await ctx.reset_visibility() + return { + "reset": True, + "default_groups": sorted(DEFAULT_ENABLED_GROUPS), + "message": "Tool visibility restored to server defaults.", + } + + return {"error": f"Unknown action '{action}'"} + + +def _list_groups() -> dict[str, Any]: + """Build the list_groups response with group metadata and tool names.""" + group_tools = get_group_tool_names() + groups = [] + for name in sorted(TOOL_GROUPS.keys()): + groups.append({ + "name": name, + "description": TOOL_GROUPS[name], + "default_enabled": name in DEFAULT_ENABLED_GROUPS, + "tools": group_tools.get(name, []), + "tool_count": len(group_tools.get(name, [])), + }) + return { + "groups": groups, + "note": ( + "Use activate/deactivate to toggle groups for this session. " + "Tools with group=None (server meta-tools) are always visible." + ), + } diff --git a/Server/src/services/tools/manage_ui.py b/Server/src/services/tools/manage_ui.py index de142f8ac..9c06063b4 100644 --- a/Server/src/services/tools/manage_ui.py +++ b/Server/src/services/tools/manage_ui.py @@ -22,6 +22,7 @@ @mcp_for_unity_tool( + group="ui", description=( "Manages Unity UI Toolkit elements (UXML documents, USS stylesheets, UIDocument components). " "Read-only actions: ping, read, get_visual_tree, list. " @@ -155,7 +156,7 @@ async def manage_ui( "Set element tooltip text. For modify_visual_element."] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) action_lower = action.lower() diff --git a/Server/src/services/tools/manage_vfx.py b/Server/src/services/tools/manage_vfx.py index 1714e5e95..91d07d703 100644 --- a/Server/src/services/tools/manage_vfx.py +++ b/Server/src/services/tools/manage_vfx.py @@ -43,6 +43,7 @@ @mcp_for_unity_tool( + group="vfx", description=( "Manage Unity VFX components (ParticleSystem, VisualEffect, LineRenderer, TrailRenderer). " "Action prefixes: particle_*, vfx_*, line_*, trail_*. " @@ -97,7 +98,7 @@ async def manage_vfx( ), } - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) params_dict: dict[str, Any] = {"action": action_normalized} if properties is not None: diff --git a/Server/src/services/tools/read_console.py b/Server/src/services/tools/read_console.py index eeb4cc664..54c933ecc 100644 --- a/Server/src/services/tools/read_console.py +++ b/Server/src/services/tools/read_console.py @@ -47,7 +47,7 @@ async def read_console( ) -> dict[str, Any]: # Get active instance from session state # Removed session_state import - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) # Set defaults if values are None action = action if action is not None else 'get' diff --git a/Server/src/services/tools/refresh_unity.py b/Server/src/services/tools/refresh_unity.py index 42eed3239..bc147dc09 100644 --- a/Server/src/services/tools/refresh_unity.py +++ b/Server/src/services/tools/refresh_unity.py @@ -182,7 +182,7 @@ async def refresh_unity( wait_for_ready: Annotated[bool, "If true, wait until editor_state.advice.ready_for_tools is true"] = True, ) -> MCPResponse | dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) params: dict[str, Any] = { "mode": mode, diff --git a/Server/src/services/tools/run_tests.py b/Server/src/services/tools/run_tests.py index 44cd385ba..05a478888 100644 --- a/Server/src/services/tools/run_tests.py +++ b/Server/src/services/tools/run_tests.py @@ -144,6 +144,7 @@ class GetTestJobResponse(MCPResponse): @mcp_for_unity_tool( + group="testing", description="Starts a Unity test run asynchronously and returns a job_id immediately. Poll with get_test_job for progress.", annotations=ToolAnnotations( title="Run Tests", @@ -167,7 +168,7 @@ async def run_tests( include_details: Annotated[bool, "Include details for all tests (default: false)"] = False, ) -> RunTestsStartResponse | MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) gate = await preflight(ctx, requires_no_tests=True, wait_for_no_compile=True, refresh_if_dirty=True) if isinstance(gate, MCPResponse): @@ -212,6 +213,7 @@ def _coerce_string_list(value) -> list[str] | None: @mcp_for_unity_tool( + group="testing", description="Polls an async Unity test job by job_id.", annotations=ToolAnnotations( title="Get Test Job", @@ -230,7 +232,7 @@ async def get_test_job( "Reduces polling frequency and avoids client-side loop detection. " "Recommended: 30-60 seconds. Returns immediately if tests complete sooner."] = None, ) -> GetTestJobResponse | MCPResponse: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) params: dict[str, Any] = {"job_id": job_id} if include_failed_tests: diff --git a/Server/src/services/tools/script_apply_edits.py b/Server/src/services/tools/script_apply_edits.py index 55fa109dd..7b8911387 100644 --- a/Server/src/services/tools/script_apply_edits.py +++ b/Server/src/services/tools/script_apply_edits.py @@ -736,7 +736,7 @@ async def script_apply_edits( namespace: Annotated[str, "Namespace of the script to edit"] | None = None, ) -> dict[str, Any]: - unity_instance = get_unity_instance_from_context(ctx) + unity_instance = await get_unity_instance_from_context(ctx) await ctx.info( f"Processing script_apply_edits: {name} (unity_instance={unity_instance or 'default'})") diff --git a/Server/src/services/tools/set_active_instance.py b/Server/src/services/tools/set_active_instance.py index ecdfb3a5c..6b90b351d 100644 --- a/Server/src/services/tools/set_active_instance.py +++ b/Server/src/services/tools/set_active_instance.py @@ -13,6 +13,7 @@ @mcp_for_unity_tool( unity_target=None, + group=None, description="Set the active Unity instance for this client/session. Accepts Name@hash, hash prefix, or port number (stdio only).", annotations=ToolAnnotations( title="Set Active Instance", @@ -47,21 +48,21 @@ async def set_active_instance( } resolved_id = match.id middleware = get_unity_instance_middleware() - middleware.set_active_instance(ctx, resolved_id) + await middleware.set_active_instance(ctx, resolved_id) return { "success": True, "message": f"Active instance set to {resolved_id}", "data": { "instance": resolved_id, - "session_key": middleware.get_session_key(ctx), + "session_key": await middleware.get_session_key(ctx), }, } # Discover running instances based on transport if transport == "http": # In remote-hosted mode, filter sessions by user_id - user_id = ctx.get_state( - "user_id") if config.http_remote_hosted else None + user_id = (await ctx.get_state( + "user_id")) if config.http_remote_hosted else None sessions_data = await PluginHub.get_sessions(user_id=user_id) sessions = sessions_data.sessions instances = [] @@ -141,8 +142,8 @@ async def set_active_instance( middleware = get_unity_instance_middleware() # We use middleware.set_active_instance to persist the selection. # The session key is an internal detail but useful for debugging response. - middleware.set_active_instance(ctx, resolved.id) - session_key = middleware.get_session_key(ctx) + await middleware.set_active_instance(ctx, resolved.id) + session_key = await middleware.get_session_key(ctx) return { "success": True, diff --git a/Server/src/transport/plugin_hub.py b/Server/src/transport/plugin_hub.py index 116f93ff9..23e2ff4e9 100644 --- a/Server/src/transport/plugin_hub.py +++ b/Server/src/transport/plugin_hub.py @@ -7,7 +7,7 @@ import os import time import uuid -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar from starlette.endpoints import WebSocketEndpoint from starlette.websockets import WebSocket, WebSocketState @@ -17,6 +17,9 @@ from models.models import MCPResponse from transport.plugin_registry import PluginRegistry from services.api_key_service import ApiKeyService + +if TYPE_CHECKING: + from fastmcp import FastMCP from transport.models import ( WelcomeMessage, RegisteredMessage, @@ -78,6 +81,10 @@ class PluginHub(WebSocketEndpoint): "read_console", "get_editor_state", "ping"} _registry: PluginRegistry | None = None + _mcp: FastMCP | None = None + # Index into mcp._transforms where Unity's server-level overrides start. + # Transforms before this index are startup defaults; at and after are Unity syncs. + _unity_transform_start: int | None = None _connections: dict[str, WebSocket] = {} # command_id -> {"future": Future, "session_id": str} _pending: dict[str, dict[str, Any]] = {} @@ -93,8 +100,10 @@ def configure( cls, registry: PluginRegistry, loop: asyncio.AbstractEventLoop | None = None, + mcp: FastMCP | None = None, ) -> None: cls._registry = registry + cls._mcp = mcp cls._loop = loop or asyncio.get_running_loop() # Ensure coordination primitives are bound to the configured loop cls._lock = asyncio.Lock() @@ -425,6 +434,10 @@ async def _handle_register_tools(self, websocket: WebSocket, payload: RegisterTo logger.info( f"Registered {len(payload.tools)} tools for session {session_id}") + # Sync server-level FastMCP visibility so new MCP client sessions + # (e.g. new Claude Code conversations) see the correct tool set. + self._sync_server_tool_visibility(payload.tools) + try: from services.custom_tool_service import CustomToolService @@ -442,6 +455,78 @@ async def _handle_register_tools(self, websocket: WebSocket, payload: RegisterTo exc_info=exc, ) + @classmethod + def _sync_server_tool_visibility(cls, registered_tools: list) -> None: + """Sync FastMCP server-level tool group visibility to match Unity's state. + + When Unity sends ``register_tools``, some groups may have been toggled + on/off via the Unity Editor GUI. We mirror that state at the FastMCP + server level so that **new** MCP client sessions (e.g. a fresh Claude + Code conversation) see the correct tool set without requiring + ``manage_tools`` activation. + + The startup ``register_all_tools()`` disables non-default groups via + ``mcp.disable(tags=...)``. Here we append ``mcp.enable(tags=...)`` + transforms for groups that Unity has enabled, effectively overriding + the startup defaults. FastMCP processes transforms in order so later + ``enable`` calls override earlier ``disable`` calls. + """ + mcp = cls._mcp + if mcp is None: + return + + try: + from services.registry import get_group_tool_names, TOOL_GROUPS + + registered_names: set[str] = set() + for tool in registered_tools: + name = getattr(tool, "name", None) if not isinstance(tool, dict) else tool.get("name") + if isinstance(name, str) and name: + registered_names.add(name) + + group_tools = get_group_tool_names() + + # Reset Unity overrides: trim transforms back to where Unity started, + # then re-apply based on current registered tools. + if cls._unity_transform_start is not None: + mcp._transforms = mcp._transforms[:cls._unity_transform_start] + else: + # First time: record where startup transforms end. + cls._unity_transform_start = len(mcp._transforms) + + enabled_groups: list[str] = [] + disabled_groups: list[str] = [] + + for group_name in sorted(TOOL_GROUPS.keys()): + tool_names = group_tools.get(group_name, []) + has_any_registered = any(n in registered_names for n in tool_names) + + if has_any_registered: + # Override the startup disable with an enable. + tag = f"group:{group_name}" + mcp.enable(tags={tag}, components={"tool"}) + enabled_groups.append(group_name) + else: + # Group not present in Unity's registered tools — disable it. + tag = f"group:{group_name}" + mcp.disable(tags={tag}, components={"tool"}) + disabled_groups.append(group_name) + + if enabled_groups or disabled_groups: + logger.info( + "Server-level tool visibility synced from Unity: " + "enabled=[%s], disabled=[%s], total_transforms=%d, unity_start=%d", + ", ".join(enabled_groups), + ", ".join(disabled_groups), + len(mcp._transforms), + cls._unity_transform_start or 0, + ) + except Exception: + logger.debug( + "Failed to sync server-level tool visibility", + exc_info=True, + ) + async def _handle_command_result(self, payload: CommandResultMessage) -> None: cls = type(self) lock = cls._lock diff --git a/Server/src/transport/unity_instance_middleware.py b/Server/src/transport/unity_instance_middleware.py index e8aea6625..86db34cb3 100644 --- a/Server/src/transport/unity_instance_middleware.py +++ b/Server/src/transport/unity_instance_middleware.py @@ -15,6 +15,8 @@ from transport.plugin_hub import PluginHub logger = logging.getLogger("mcp-for-unity-server") +# Separate logger that propagates to root -> stderr so diagnostics show in console +_diag = logging.getLogger("transport.unity_instance_middleware") # Store a global reference to the middleware instance so tools can interact # with it to set or clear the active unity instance. @@ -66,7 +68,7 @@ def __init__(self): self._tool_visibility_refresh_interval_seconds = 0.5 self._has_logged_empty_registry_warning = False - def get_session_key(self, ctx) -> str: + async def get_session_key(self, ctx) -> str: """ Derive a stable key for the calling session. @@ -79,28 +81,28 @@ def get_session_key(self, ctx) -> str: return client_id # In remote-hosted mode, use user_id so different users get isolated instance selections - user_id = ctx.get_state("user_id") + user_id = await ctx.get_state("user_id") if isinstance(user_id, str) and user_id: return f"user:{user_id}" # Fallback to global for local dev stability return "global" - def set_active_instance(self, ctx, instance_id: str) -> None: + async def set_active_instance(self, ctx, instance_id: str) -> None: """Store the active instance for this session.""" - key = self.get_session_key(ctx) + key = await self.get_session_key(ctx) with self._lock: self._active_by_key[key] = instance_id - def get_active_instance(self, ctx) -> str | None: + async def get_active_instance(self, ctx) -> str | None: """Retrieve the active instance for this session.""" - key = self.get_session_key(ctx) + key = await self.get_session_key(ctx) with self._lock: return self._active_by_key.get(key) - def clear_active_instance(self, ctx) -> None: + async def clear_active_instance(self, ctx) -> None: """Clear the stored instance for this session.""" - key = self.get_session_key(ctx) + key = await self.get_session_key(ctx) with self._lock: self._active_by_key.pop(key, None) @@ -119,7 +121,7 @@ async def _discover_instances(self, ctx) -> list: user_id = None get_state_fn = getattr(ctx, "get_state", None) if callable(get_state_fn) and config.http_remote_hosted: - user_id = get_state_fn("user_id") + user_id = await get_state_fn("user_id") sessions_data = await PluginHub.get_sessions(user_id=user_id) sessions = sessions_data.sessions or {} for session_info in sessions.values(): @@ -248,7 +250,7 @@ async def _maybe_autoselect_instance(self, ctx) -> str | None: ids.append(f"{project}@{hash_value}") if len(ids) == 1: chosen = ids[0] - self.set_active_instance(ctx, chosen) + await self.set_active_instance(ctx, chosen) logger.info( "Auto-selected sole Unity instance via PluginHub: %s", chosen, @@ -286,7 +288,7 @@ async def _maybe_autoselect_instance(self, ctx) -> str | None: ids = [inst_id for inst_id in ids if inst_id] if len(ids) == 1: chosen = ids[0] - self.set_active_instance(ctx, chosen) + await self.set_active_instance(ctx, chosen) logger.info( "Auto-selected sole Unity instance via stdio discovery: %s", chosen, @@ -342,7 +344,7 @@ async def _inject_unity_instance(self, context: MiddlewareContext) -> None: "API key authentication required. Provide a valid X-API-Key header." ) if user_id: - ctx.set_state("user_id", user_id) + await ctx.set_state("user_id", user_id) # Per-call routing: check if this tool call explicitly specifies unity_instance. # context.message.arguments is a mutable dict on CallToolRequestParams; resource @@ -360,7 +362,7 @@ async def _inject_unity_instance(self, context: MiddlewareContext) -> None: logger.debug("Per-call unity_instance resolved to: %s", active_instance) if not active_instance: - active_instance = self.get_active_instance(ctx) + active_instance = await self.get_active_instance(ctx) if not active_instance: active_instance = await self._maybe_autoselect_instance(ctx) if active_instance: @@ -403,9 +405,9 @@ async def _inject_unity_instance(self, context: MiddlewareContext) -> None: exc_info=True ) - ctx.set_state("unity_instance", active_instance) + await ctx.set_state("unity_instance", active_instance) if session_id is not None: - ctx.set_state("unity_session_id", session_id) + await ctx.set_state("unity_session_id", session_id) async def on_call_tool(self, context: MiddlewareContext, call_next): """Inject active Unity instance into tool context if available.""" @@ -419,15 +421,30 @@ async def on_read_resource(self, context: MiddlewareContext, call_next): async def on_list_tools(self, context: MiddlewareContext, call_next): """Filter MCP tool listing to the Unity-enabled set when session data is available.""" - await self._inject_unity_instance(context) + try: + await self._inject_unity_instance(context) + except Exception as exc: + _diag.warning( + "on_list_tools: _inject_unity_instance failed (%s: %s), continuing without instance", + type(exc).__name__, exc, + ) + tools = await call_next(context) + tool_names_from_fastmcp = sorted(getattr(t, "name", "?") for t in tools) + _diag.info( + "on_list_tools: FastMCP returned %d tools: %s", + len(tools), tool_names_from_fastmcp, + ) + if not self._should_filter_tool_listing(): + _diag.info("on_list_tools: skipping middleware filter (not HTTP or PluginHub not configured)") return tools self._refresh_tool_visibility_metadata_from_registry() enabled_tool_names = await self._resolve_enabled_tool_names_for_context(context) if enabled_tool_names is None: + _diag.info("on_list_tools: no Unity session data, returning %d tools from FastMCP as-is", len(tools)) return tools filtered = [] @@ -436,6 +453,11 @@ async def on_list_tools(self, context: MiddlewareContext, call_next): if self._is_tool_visible(tool_name, enabled_tool_names): filtered.append(tool) + _diag.info( + "on_list_tools: filtered %d/%d tools visible (Unity register_tools). " + "enabled_names=%s", + len(filtered), len(tools), sorted(enabled_tool_names), + ) return filtered def _should_filter_tool_listing(self) -> bool: @@ -447,8 +469,8 @@ async def _resolve_enabled_tool_names_for_context( context: MiddlewareContext, ) -> set[str] | None: ctx = context.fastmcp_context - user_id = ctx.get_state("user_id") if config.http_remote_hosted else None - active_instance = ctx.get_state("unity_instance") + user_id = (await ctx.get_state("user_id")) if config.http_remote_hosted else None + active_instance = await ctx.get_state("unity_instance") project_hashes = self._resolve_candidate_project_hashes(active_instance) try: sessions_data = await PluginHub.get_sessions(user_id=user_id) diff --git a/Server/tests/integration/test_debug_request_context_diagnostics.py b/Server/tests/integration/test_debug_request_context_diagnostics.py index 8b25a57f7..3ebccae07 100644 --- a/Server/tests/integration/test_debug_request_context_diagnostics.py +++ b/Server/tests/integration/test_debug_request_context_diagnostics.py @@ -1,7 +1,8 @@ import pytest -def test_debug_request_context_includes_server_diagnostics(monkeypatch): +@pytest.mark.asyncio +async def test_debug_request_context_includes_server_diagnostics(monkeypatch): # Import inside test so stubs in conftest are applied. import services.tools.debug_request_context as mod @@ -11,13 +12,13 @@ class DummyCtx: session_id = None client_id = None - def get_state(self, _k): + async def get_state(self, _k): return None # Ensure get_package_version is stable for assertion monkeypatch.setattr(mod, "get_package_version", lambda: "9.9.9-test") - res = mod.debug_request_context(DummyCtx()) + res = await mod.debug_request_context(DummyCtx()) assert res.get("success") is True data = res.get("data") or {} server = data.get("server") or {} diff --git a/Server/tests/integration/test_helpers.py b/Server/tests/integration/test_helpers.py index cfa559387..83b30b18e 100644 --- a/Server/tests/integration/test_helpers.py +++ b/Server/tests/integration/test_helpers.py @@ -46,11 +46,11 @@ async def warn(self, message): async def error(self, message): self.log_error.append(message) - def set_state(self, key, value): + async def set_state(self, key, value): """Set state value (mimics FastMCP context.set_state)""" self._state[key] = value - def get_state(self, key, default=None): + async def get_state(self, key, default=None): """Get state value (mimics FastMCP context.get_state)""" return self._state.get(key, default) diff --git a/Server/tests/integration/test_inline_unity_instance.py b/Server/tests/integration/test_inline_unity_instance.py index 129e67a52..280ad47f9 100644 --- a/Server/tests/integration/test_inline_unity_instance.py +++ b/Server/tests/integration/test_inline_unity_instance.py @@ -6,7 +6,6 @@ 2. Resolves it to a validated instance identifier 3. Sets it in request-scoped state for that call only (does NOT persist to session) """ -import asyncio import sys import types from types import SimpleNamespace @@ -76,7 +75,8 @@ async def fake_discover(ctx): # Pop behaviour # --------------------------------------------------------------------------- -def test_unity_instance_is_popped_from_arguments(monkeypatch): +@pytest.mark.asyncio +async def test_unity_instance_is_popped_from_arguments(monkeypatch): """unity_instance key must be removed from arguments before the tool function sees them.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123")] mw = _make_middleware(monkeypatch, pool_instances=instances) @@ -86,25 +86,26 @@ def test_unity_instance_is_popped_from_arguments(monkeypatch): args = {"action": "get_active", "unity_instance": "abc123"} mw_ctx = DummyMiddlewareContext(ctx, arguments=args) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) assert "unity_instance" not in args assert "action" in args # other keys untouched -def test_arguments_without_unity_instance_untouched(monkeypatch): +@pytest.mark.asyncio +async def test_arguments_without_unity_instance_untouched(monkeypatch): """When unity_instance is absent, arguments dict is left completely untouched.""" mw = _make_middleware(monkeypatch, pool_instances=[SimpleNamespace(id="Proj@abc123", hash="abc123")]) ctx = DummyContext() ctx.client_id = "client-1" # Seed a persisted instance so auto-select isn't needed - mw.set_active_instance(ctx, "Proj@abc123") + await mw.set_active_instance(ctx, "Proj@abc123") args = {"action": "get_active", "name": "Test"} mw_ctx = DummyMiddlewareContext(ctx, arguments=args) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) assert args == {"action": "get_active", "name": "Test"} @@ -113,7 +114,8 @@ def test_arguments_without_unity_instance_untouched(monkeypatch): # Per-call routing (no persistence) # --------------------------------------------------------------------------- -def test_inline_routes_to_specified_instance(monkeypatch): +@pytest.mark.asyncio +async def test_inline_routes_to_specified_instance(monkeypatch): """Per-call unity_instance sets request state to the resolved instance.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123")] mw = _make_middleware(monkeypatch, pool_instances=instances) @@ -122,12 +124,13 @@ def test_inline_routes_to_specified_instance(monkeypatch): ctx.client_id = "client-1" mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "abc123"}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" -def test_inline_does_not_persist_to_session(monkeypatch): +@pytest.mark.asyncio +async def test_inline_does_not_persist_to_session(monkeypatch): """Per-call unity_instance must not change the session-persisted instance.""" instances = [ SimpleNamespace(id="ProjA@aaa111", hash="aaa111"), @@ -137,20 +140,21 @@ def test_inline_does_not_persist_to_session(monkeypatch): ctx = DummyContext() ctx.client_id = "client-1" - mw.set_active_instance(ctx, "ProjA@aaa111") + await mw.set_active_instance(ctx, "ProjA@aaa111") # Call 1: inline override to ProjB mw_ctx1 = DummyMiddlewareContext(ctx, arguments={"unity_instance": "bbb222"}) - asyncio.run(mw._inject_unity_instance(mw_ctx1)) - assert ctx.get_state("unity_instance") == "ProjB@bbb222" + await mw._inject_unity_instance(mw_ctx1) + assert await ctx.get_state("unity_instance") == "ProjB@bbb222" # Call 2: no inline — must revert to session-persisted ProjA mw_ctx2 = DummyMiddlewareContext(ctx, arguments={}) - asyncio.run(mw._inject_unity_instance(mw_ctx2)) - assert ctx.get_state("unity_instance") == "ProjA@aaa111" + await mw._inject_unity_instance(mw_ctx2) + assert await ctx.get_state("unity_instance") == "ProjA@aaa111" -def test_inline_overrides_session_persisted_instance(monkeypatch): +@pytest.mark.asyncio +async def test_inline_overrides_session_persisted_instance(monkeypatch): """Inline unity_instance takes precedence over session-persisted instance.""" instances = [ SimpleNamespace(id="ProjA@aaa111", hash="aaa111"), @@ -160,21 +164,22 @@ def test_inline_overrides_session_persisted_instance(monkeypatch): ctx = DummyContext() ctx.client_id = "client-1" - mw.set_active_instance(ctx, "ProjA@aaa111") + await mw.set_active_instance(ctx, "ProjA@aaa111") mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "ProjB@bbb222"}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "ProjB@bbb222" + assert await ctx.get_state("unity_instance") == "ProjB@bbb222" # Session still pinned to ProjA - assert mw.get_active_instance(ctx) == "ProjA@aaa111" + assert await mw.get_active_instance(ctx) == "ProjA@aaa111" # --------------------------------------------------------------------------- # Port number resolution (stdio) # --------------------------------------------------------------------------- -def test_port_number_resolves_to_name_hash_stdio(monkeypatch): +@pytest.mark.asyncio +async def test_port_number_resolves_to_name_hash_stdio(monkeypatch): """Bare port number resolves to the matching Name@hash in stdio mode.""" instances = [ SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401), @@ -186,12 +191,13 @@ def test_port_number_resolves_to_name_hash_stdio(monkeypatch): ctx.client_id = "client-1" mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "6401"}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" -def test_port_number_not_found_raises(monkeypatch): +@pytest.mark.asyncio +async def test_port_number_not_found_raises(monkeypatch): """Port number with no matching instance raises ValueError.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401)] mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances) @@ -201,10 +207,11 @@ def test_port_number_not_found_raises(monkeypatch): mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "9999"}) with pytest.raises(ValueError, match="No Unity instance found on port 9999"): - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) -def test_port_number_errors_in_http_mode(monkeypatch): +@pytest.mark.asyncio +async def test_port_number_errors_in_http_mode(monkeypatch): """Bare port number raises ValueError in HTTP transport mode.""" mw = _make_middleware(monkeypatch, transport="http") @@ -213,14 +220,15 @@ def test_port_number_errors_in_http_mode(monkeypatch): mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "6401"}) with pytest.raises(ValueError, match="not supported in HTTP transport mode"): - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) # --------------------------------------------------------------------------- # Name@hash and hash prefix resolution # --------------------------------------------------------------------------- -def test_name_at_hash_resolves_exactly(monkeypatch): +@pytest.mark.asyncio +async def test_name_at_hash_resolves_exactly(monkeypatch): """Full Name@hash resolves directly without discovery.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123")] mw = _make_middleware(monkeypatch, pool_instances=instances) @@ -229,12 +237,13 @@ def test_name_at_hash_resolves_exactly(monkeypatch): ctx.client_id = "client-1" mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "Proj@abc123"}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" -def test_unknown_name_at_hash_raises(monkeypatch): +@pytest.mark.asyncio +async def test_unknown_name_at_hash_raises(monkeypatch): """Unknown Name@hash raises ValueError.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123")] mw = _make_middleware(monkeypatch, pool_instances=instances) @@ -244,10 +253,11 @@ def test_unknown_name_at_hash_raises(monkeypatch): mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "Ghost@deadbeef"}) with pytest.raises(ValueError, match="not found"): - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) -def test_hash_prefix_resolves_unique(monkeypatch): +@pytest.mark.asyncio +async def test_hash_prefix_resolves_unique(monkeypatch): """Unique hash prefix resolves to the full Name@hash.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123")] mw = _make_middleware(monkeypatch, pool_instances=instances) @@ -256,12 +266,13 @@ def test_hash_prefix_resolves_unique(monkeypatch): ctx.client_id = "client-1" mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "abc"}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" -def test_ambiguous_hash_prefix_raises(monkeypatch): +@pytest.mark.asyncio +async def test_ambiguous_hash_prefix_raises(monkeypatch): """Ambiguous hash prefix raises ValueError.""" instances = [ SimpleNamespace(id="ProjA@abc111", hash="abc111"), @@ -274,10 +285,11 @@ def test_ambiguous_hash_prefix_raises(monkeypatch): mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "abc"}) with pytest.raises(ValueError, match="ambiguous"): - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) -def test_no_match_raises(monkeypatch): +@pytest.mark.asyncio +async def test_no_match_raises(monkeypatch): """Hash prefix matching nothing raises ValueError.""" instances = [SimpleNamespace(id="Proj@abc123", hash="abc123")] mw = _make_middleware(monkeypatch, pool_instances=instances) @@ -287,47 +299,50 @@ def test_no_match_raises(monkeypatch): mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "xyz"}) with pytest.raises(ValueError, match="No running Unity instance"): - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) # --------------------------------------------------------------------------- # Edge cases # --------------------------------------------------------------------------- -def test_none_unity_instance_falls_through_to_session(monkeypatch): +@pytest.mark.asyncio +async def test_none_unity_instance_falls_through_to_session(monkeypatch): """None value for unity_instance falls through to session-persisted instance.""" mw = _make_middleware(monkeypatch) ctx = DummyContext() ctx.client_id = "client-1" - mw.set_active_instance(ctx, "Proj@abc123") + await mw.set_active_instance(ctx, "Proj@abc123") mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": None, "action": "x"}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" -def test_empty_string_unity_instance_falls_through_to_session(monkeypatch): +@pytest.mark.asyncio +async def test_empty_string_unity_instance_falls_through_to_session(monkeypatch): """Empty string unity_instance falls through to session-persisted instance.""" mw = _make_middleware(monkeypatch) ctx = DummyContext() ctx.client_id = "client-1" - mw.set_active_instance(ctx, "Proj@abc123") + await mw.set_active_instance(ctx, "Proj@abc123") mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": " "}) - asyncio.run(mw._inject_unity_instance(mw_ctx)) + await mw._inject_unity_instance(mw_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" -def test_resource_read_unaffected(monkeypatch): +@pytest.mark.asyncio +async def test_resource_read_unaffected(monkeypatch): """on_read_resource with no .arguments attribute routes via session state normally.""" mw = _make_middleware(monkeypatch) ctx = DummyContext() ctx.client_id = "client-1" - mw.set_active_instance(ctx, "Proj@abc123") + await mw.set_active_instance(ctx, "Proj@abc123") # ReadResourceRequestParams has .uri not .arguments resource_ctx = SimpleNamespace( @@ -335,16 +350,17 @@ def test_resource_read_unaffected(monkeypatch): message=SimpleNamespace(uri="mcpforunity://scene/active"), ) - asyncio.run(mw._inject_unity_instance(resource_ctx)) + await mw._inject_unity_instance(resource_ctx) - assert ctx.get_state("unity_instance") == "Proj@abc123" + assert await ctx.get_state("unity_instance") == "Proj@abc123" # --------------------------------------------------------------------------- # set_active_instance tool: port number support # --------------------------------------------------------------------------- -def test_set_active_instance_port_stdio(monkeypatch): +@pytest.mark.asyncio +async def test_set_active_instance_port_stdio(monkeypatch): """set_active_instance accepts a port number in stdio mode and resolves to Name@hash.""" monkeypatch.setattr(config, "transport_mode", "stdio") monkeypatch.setattr(config, "http_remote_hosted", False) @@ -367,14 +383,15 @@ def discover_all_instances(self, force_refresh=False): ctx = DummyContext() ctx.client_id = "client-1" - result = asyncio.run(set_active_instance(ctx, instance="6401")) + result = await set_active_instance(ctx, instance="6401") assert result["success"] is True assert result["data"]["instance"] == "Proj@abc123" - assert mw.get_active_instance(ctx) == "Proj@abc123" + assert await mw.get_active_instance(ctx) == "Proj@abc123" -def test_set_active_instance_port_http_errors(monkeypatch): +@pytest.mark.asyncio +async def test_set_active_instance_port_http_errors(monkeypatch): """set_active_instance rejects port numbers in HTTP mode.""" monkeypatch.setattr(config, "transport_mode", "http") monkeypatch.setattr(config, "http_remote_hosted", False) @@ -384,7 +401,7 @@ def test_set_active_instance_port_http_errors(monkeypatch): ctx = DummyContext() ctx.client_id = "client-1" - result = asyncio.run(set_active_instance(ctx, instance="6401")) + result = await set_active_instance(ctx, instance="6401") assert result["success"] is False assert "not supported in HTTP transport mode" in result["error"] @@ -394,7 +411,8 @@ def test_set_active_instance_port_http_errors(monkeypatch): # batch_execute rejects inner unity_instance # --------------------------------------------------------------------------- -def test_batch_execute_rejects_inner_unity_instance(): +@pytest.mark.asyncio +async def test_batch_execute_rejects_inner_unity_instance(): """batch_execute raises ValueError when an inner command contains unity_instance.""" from services.tools.batch_execute import batch_execute @@ -407,4 +425,4 @@ def test_batch_execute_rejects_inner_unity_instance(): ] with pytest.raises(ValueError, match="Per-command instance routing is not supported inside batch_execute"): - asyncio.run(batch_execute(ctx, commands=commands)) + await batch_execute(ctx, commands=commands) diff --git a/Server/tests/integration/test_instance_autoselect.py b/Server/tests/integration/test_instance_autoselect.py index 684c6ae65..b974cb11c 100644 --- a/Server/tests/integration/test_instance_autoselect.py +++ b/Server/tests/integration/test_instance_autoselect.py @@ -1,4 +1,4 @@ -import asyncio +import pytest import sys import types from types import SimpleNamespace @@ -12,7 +12,8 @@ def __init__(self, ctx): self.fastmcp_context = ctx -def test_auto_selects_single_instance_via_pluginhub(monkeypatch): +@pytest.mark.asyncio +async def test_auto_selects_single_instance_via_pluginhub(monkeypatch): plugin_hub = types.ModuleType("transport.plugin_hub") class PluginHub: @@ -50,19 +51,20 @@ async def fake_get_sessions(): monkeypatch.setattr(plugin_hub.PluginHub, "get_sessions", fake_get_sessions) - selected = asyncio.run(middleware._maybe_autoselect_instance(ctx)) + selected = await middleware._maybe_autoselect_instance(ctx) assert selected == "Ramble@deadbeef" - assert middleware.get_active_instance(ctx) == "Ramble@deadbeef" + assert await middleware.get_active_instance(ctx) == "Ramble@deadbeef" assert call_count["sessions"] == 1 - asyncio.run(middleware._inject_unity_instance(middleware_context)) + await middleware._inject_unity_instance(middleware_context) - assert ctx.get_state("unity_instance") == "Ramble@deadbeef" + assert await ctx.get_state("unity_instance") == "Ramble@deadbeef" assert call_count["sessions"] == 1 -def test_auto_selects_single_instance_via_stdio(monkeypatch): +@pytest.mark.asyncio +async def test_auto_selects_single_instance_via_stdio(monkeypatch): plugin_hub = types.ModuleType("transport.plugin_hub") class PluginHub: @@ -93,17 +95,18 @@ def discover_all_instances(self, force_refresh=False): unity_connection.get_unity_connection_pool = lambda: PoolStub() monkeypatch.setitem(sys.modules, "transport.legacy.unity_connection", unity_connection) - selected = asyncio.run(middleware._maybe_autoselect_instance(ctx)) + selected = await middleware._maybe_autoselect_instance(ctx) assert selected == "UnityMCPTests@cc8756d4" - assert middleware.get_active_instance(ctx) == "UnityMCPTests@cc8756d4" + assert await middleware.get_active_instance(ctx) == "UnityMCPTests@cc8756d4" - asyncio.run(middleware._inject_unity_instance(middleware_context)) + await middleware._inject_unity_instance(middleware_context) - assert ctx.get_state("unity_instance") == "UnityMCPTests@cc8756d4" + assert await ctx.get_state("unity_instance") == "UnityMCPTests@cc8756d4" -def test_auto_select_handles_stdio_errors(monkeypatch): +@pytest.mark.asyncio +async def test_auto_select_handles_stdio_errors(monkeypatch): plugin_hub = types.ModuleType("transport.plugin_hub") class PluginHub: @@ -130,7 +133,7 @@ def discover_all_instances(self, force_refresh=False): unity_connection.get_unity_connection_pool = lambda: PoolStub() monkeypatch.setitem(sys.modules, "transport.legacy.unity_connection", unity_connection) - selected = asyncio.run(middleware._maybe_autoselect_instance(ctx)) + selected = await middleware._maybe_autoselect_instance(ctx) assert selected is None - assert middleware.get_active_instance(ctx) is None + assert await middleware.get_active_instance(ctx) is None diff --git a/Server/tests/integration/test_instance_routing_comprehensive.py b/Server/tests/integration/test_instance_routing_comprehensive.py index e96e69685..4a8ac3d68 100644 --- a/Server/tests/integration/test_instance_routing_comprehensive.py +++ b/Server/tests/integration/test_instance_routing_comprehensive.py @@ -24,7 +24,8 @@ class TestInstanceRoutingBasics: """Test basic middleware functionality.""" - def test_middleware_stores_and_retrieves_instance(self): + @pytest.mark.asyncio + async def test_middleware_stores_and_retrieves_instance(self): """Middleware should store and retrieve instance per session.""" middleware = UnityInstanceMiddleware() ctx = Mock(spec=Context) @@ -32,12 +33,13 @@ def test_middleware_stores_and_retrieves_instance(self): ctx.client_id = "test-client-1" # Set active instance - middleware.set_active_instance(ctx, "TestProject@abc123") + await middleware.set_active_instance(ctx, "TestProject@abc123") # Retrieve should return same instance - assert middleware.get_active_instance(ctx) == "TestProject@abc123" + assert await middleware.get_active_instance(ctx) == "TestProject@abc123" - def test_middleware_isolates_sessions(self): + @pytest.mark.asyncio + async def test_middleware_isolates_sessions(self): """Different sessions should have independent instance selections.""" middleware = UnityInstanceMiddleware() @@ -50,14 +52,15 @@ def test_middleware_isolates_sessions(self): ctx2.client_id = "client-2" # Set different instances for different sessions - middleware.set_active_instance(ctx1, "Project1@aaa") - middleware.set_active_instance(ctx2, "Project2@bbb") + await middleware.set_active_instance(ctx1, "Project1@aaa") + await middleware.set_active_instance(ctx2, "Project2@bbb") # Each session should retrieve its own instance - assert middleware.get_active_instance(ctx1) == "Project1@aaa" - assert middleware.get_active_instance(ctx2) == "Project2@bbb" + assert await middleware.get_active_instance(ctx1) == "Project1@aaa" + assert await middleware.get_active_instance(ctx2) == "Project2@bbb" - def test_middleware_fallback_to_client_id(self): + @pytest.mark.asyncio + async def test_middleware_fallback_to_client_id(self): """When session_id unavailable, should use client_id.""" middleware = UnityInstanceMiddleware() @@ -65,20 +68,21 @@ def test_middleware_fallback_to_client_id(self): ctx.session_id = None ctx.client_id = "client-123" - middleware.set_active_instance(ctx, "Project@xyz") - assert middleware.get_active_instance(ctx) == "Project@xyz" + await middleware.set_active_instance(ctx, "Project@xyz") + assert await middleware.get_active_instance(ctx) == "Project@xyz" - def test_middleware_fallback_to_global(self): + @pytest.mark.asyncio + async def test_middleware_fallback_to_global(self): """When no session/client id, should use 'global' key.""" middleware = UnityInstanceMiddleware() ctx = Mock(spec=Context) ctx.session_id = None ctx.client_id = None - ctx.get_state = Mock(return_value=None) + ctx.get_state = AsyncMock(return_value=None) - middleware.set_active_instance(ctx, "Project@global") - assert middleware.get_active_instance(ctx) == "Project@global" + await middleware.set_active_instance(ctx, "Project@global") + assert await middleware.get_active_instance(ctx) == "Project@global" class TestInstanceRoutingIntegration: @@ -93,16 +97,16 @@ async def test_middleware_injects_state_into_context(self): ctx = Mock(spec=Context) ctx.session_id = "test-session" state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) # Create middleware context middleware_ctx = Mock() middleware_ctx.fastmcp_context = ctx # Set active instance - middleware.set_active_instance(ctx, "TestProject@abc123") + await middleware.set_active_instance(ctx, "TestProject@abc123") # Mock call_next async def mock_call_next(ctx): @@ -115,29 +119,31 @@ async def mock_call_next(ctx): ctx.set_state.assert_called_once_with( "unity_instance", "TestProject@abc123") - def test_get_unity_instance_from_context_checks_state(self): + @pytest.mark.asyncio + async def test_get_unity_instance_from_context_checks_state(self): """get_unity_instance_from_context must read from ctx.get_state().""" ctx = Mock(spec=Context) # Set up state storage (only source of truth now) state_storage = {"unity_instance": "Project@state123"} - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) # Call and verify - result = get_unity_instance_from_context(ctx) + result = await get_unity_instance_from_context(ctx) assert result == "Project@state123", \ "get_unity_instance_from_context must read from ctx.get_state()!" - def test_get_unity_instance_returns_none_when_not_set(self): + @pytest.mark.asyncio + async def test_get_unity_instance_returns_none_when_not_set(self): """Should return None when no instance is set.""" ctx = Mock(spec=Context) # Empty state storage state_storage = {} - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) - result = get_unity_instance_from_context(ctx) + result = await get_unity_instance_from_context(ctx) assert result is None @@ -151,8 +157,8 @@ def _create_mock_context_with_instance(self, instance_id: str): # Set up state storage (only source of truth) state_storage = {"unity_instance": instance_id} - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) - ctx.set_state = Mock(side_effect=lambda k, + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) return ctx @@ -169,9 +175,9 @@ async def test_set_active_instance_http_transport(self, monkeypatch): ctx = Mock(spec=Context) ctx.session_id = "http-session" state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) monkeypatch.setattr(config, "transport_mode", "http") fake_sessions = SessionList( @@ -196,7 +202,7 @@ async def test_set_active_instance_http_transport(self, monkeypatch): result = await set_active_instance_tool(ctx, "Ramble@8e29de57") assert result["success"] is True - assert middleware.get_active_instance(ctx) == "Ramble@8e29de57" + assert await middleware.get_active_instance(ctx) == "Ramble@8e29de57" @pytest.mark.asyncio async def test_set_active_instance_http_hash_only(self, monkeypatch): @@ -205,9 +211,9 @@ async def test_set_active_instance_http_hash_only(self, monkeypatch): ctx = Mock(spec=Context) ctx.session_id = "http-session-2" state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) monkeypatch.setattr(config, "transport_mode", "http") fake_sessions = SessionList( @@ -232,7 +238,7 @@ async def test_set_active_instance_http_hash_only(self, monkeypatch): result = await set_active_instance_tool(ctx, "UnityMCPTests@cc8756d4") assert result["success"] is True - assert middleware.get_active_instance(ctx) == "UnityMCPTests@cc8756d4" + assert await middleware.get_active_instance(ctx) == "UnityMCPTests@cc8756d4" @pytest.mark.asyncio async def test_set_active_instance_http_hash_missing(self, monkeypatch): @@ -297,14 +303,14 @@ async def test_rapid_instance_switching(self): ctx.session_id = "test-session" state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) instances = ["Project1@aaa", "Project2@bbb", "Project3@ccc"] for instance in instances: - middleware.set_active_instance(ctx, instance) + await middleware.set_active_instance(ctx, instance) # Create middleware context middleware_ctx = Mock() @@ -330,13 +336,13 @@ async def test_set_then_immediate_create_script(self): ctx.info = Mock() state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) ctx.request_context = None # Set active instance - middleware.set_active_instance(ctx, "ramble@8e29de57") + await middleware.set_active_instance(ctx, "ramble@8e29de57") # Simulate middleware intercepting create_script call middleware_ctx = Mock() @@ -344,7 +350,7 @@ async def test_set_then_immediate_create_script(self): async def mock_create_script_call(ctx): # This simulates what create_script does - instance = get_unity_instance_from_context(ctx) + instance = await get_unity_instance_from_context(ctx) return {"success": True, "routed_to": instance} # Inject state via middleware @@ -387,7 +393,7 @@ async def simulate_create_script(ctx, script_name, expected_instance): async def mock_tool_call(middleware_ctx): # The middleware passes the middleware_ctx, we need the fastmcp_context tool_ctx = middleware_ctx.fastmcp_context - instance = get_unity_instance_from_context(tool_ctx) + instance = await get_unity_instance_from_context(tool_ctx) script_routes[script_name] = instance return {"success": True} @@ -400,21 +406,21 @@ async def mock_tool_call(middleware_ctx): ctx.info = Mock() state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) # Execute sequence - middleware.set_active_instance(ctx, "ramble@8e29de57") + await middleware.set_active_instance(ctx, "ramble@8e29de57") expected1 = await simulate_create_script(ctx, "Script1", "ramble@8e29de57") - middleware.set_active_instance(ctx, "UnityMCPTests@cc8756d4") + await middleware.set_active_instance(ctx, "UnityMCPTests@cc8756d4") expected2 = await simulate_create_script(ctx, "Script2", "UnityMCPTests@cc8756d4") - middleware.set_active_instance(ctx, "ramble@8e29de57") + await middleware.set_active_instance(ctx, "ramble@8e29de57") expected3 = await simulate_create_script(ctx, "Script3", "ramble@8e29de57") - middleware.set_active_instance(ctx, "UnityMCPTests@cc8756d4") + await middleware.set_active_instance(ctx, "UnityMCPTests@cc8756d4") expected4 = await simulate_create_script(ctx, "Script4", "UnityMCPTests@cc8756d4") # Assertions - these will FAIL until the bug is fixed diff --git a/Server/tests/integration/test_instance_targeting_resolution.py b/Server/tests/integration/test_instance_targeting_resolution.py index 6fbfeb166..bbdef0bef 100644 --- a/Server/tests/integration/test_instance_targeting_resolution.py +++ b/Server/tests/integration/test_instance_targeting_resolution.py @@ -14,11 +14,11 @@ async def test_manage_gameobject_uses_session_state(monkeypatch): set_unity_instance_middleware(middleware) ctx = DummyContext() - middleware.set_active_instance(ctx, "SessionProj@AAAA1111") - assert middleware.get_active_instance(ctx) == "SessionProj@AAAA1111" + await middleware.set_active_instance(ctx, "SessionProj@AAAA1111") + assert await middleware.get_active_instance(ctx) == "SessionProj@AAAA1111" # Simulate middleware injection into request state - ctx.set_state("unity_instance", "SessionProj@AAAA1111") + await ctx.set_state("unity_instance", "SessionProj@AAAA1111") captured = {} @@ -60,7 +60,7 @@ async def test_manage_gameobject_without_active_instance(monkeypatch): set_unity_instance_middleware(middleware) ctx = DummyContext() - assert middleware.get_active_instance(ctx) is None + assert await middleware.get_active_instance(ctx) is None # Don't set any state in context captured = {} diff --git a/Server/tests/integration/test_manage_scriptable_object_tool.py b/Server/tests/integration/test_manage_scriptable_object_tool.py index f9d39973c..38dc44d52 100644 --- a/Server/tests/integration/test_manage_scriptable_object_tool.py +++ b/Server/tests/integration/test_manage_scriptable_object_tool.py @@ -1,10 +1,11 @@ -import asyncio +import pytest from .test_helpers import DummyContext import services.tools.manage_scriptable_object as mod -def test_manage_scriptable_object_forwards_create_params(monkeypatch): +@pytest.mark.asyncio +async def test_manage_scriptable_object_forwards_create_params(monkeypatch): captured = {} async def fake_async_send(cmd, params, **kwargs): @@ -15,9 +16,9 @@ async def fake_async_send(cmd, params, **kwargs): monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send) ctx = DummyContext() - ctx.set_state("unity_instance", "UnityMCPTests@dummy") + await ctx.set_state("unity_instance", "UnityMCPTests@dummy") - result = asyncio.run( + result = await ( mod.manage_scriptable_object( ctx=ctx, action="create", @@ -40,7 +41,8 @@ async def fake_async_send(cmd, params, **kwargs): assert captured["params"]["patches"][0]["propertyPath"] == "displayName" -def test_manage_scriptable_object_forwards_modify_params(monkeypatch): +@pytest.mark.asyncio +async def test_manage_scriptable_object_forwards_modify_params(monkeypatch): captured = {} async def fake_async_send(cmd, params, **kwargs): @@ -51,9 +53,9 @@ async def fake_async_send(cmd, params, **kwargs): monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send) ctx = DummyContext() - ctx.set_state("unity_instance", "UnityMCPTests@dummy") + await ctx.set_state("unity_instance", "UnityMCPTests@dummy") - result = asyncio.run( + result = await ( mod.manage_scriptable_object( ctx=ctx, action="modify", @@ -69,7 +71,8 @@ async def fake_async_send(cmd, params, **kwargs): assert captured["params"]["patches"][0]["op"] == "array_resize" -def test_manage_scriptable_object_forwards_dry_run_param(monkeypatch): +@pytest.mark.asyncio +async def test_manage_scriptable_object_forwards_dry_run_param(monkeypatch): captured = {} async def fake_async_send(cmd, params, **kwargs): @@ -80,9 +83,9 @@ async def fake_async_send(cmd, params, **kwargs): monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send) ctx = DummyContext() - ctx.set_state("unity_instance", "UnityMCPTests@dummy") + await ctx.set_state("unity_instance", "UnityMCPTests@dummy") - result = asyncio.run( + result = await ( mod.manage_scriptable_object( ctx=ctx, action="modify", @@ -99,7 +102,8 @@ async def fake_async_send(cmd, params, **kwargs): assert captured["params"]["target"] == {"guid": "abc123"} -def test_manage_scriptable_object_dry_run_string_coercion(monkeypatch): +@pytest.mark.asyncio +async def test_manage_scriptable_object_dry_run_string_coercion(monkeypatch): """Test that dry_run accepts string 'true' and coerces to boolean.""" captured = {} @@ -111,9 +115,9 @@ async def fake_async_send(cmd, params, **kwargs): monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send) ctx = DummyContext() - ctx.set_state("unity_instance", "UnityMCPTests@dummy") + await ctx.set_state("unity_instance", "UnityMCPTests@dummy") - result = asyncio.run( + result = await ( mod.manage_scriptable_object( ctx=ctx, action="modify", diff --git a/Server/tests/integration/test_middleware_auth_integration.py b/Server/tests/integration/test_middleware_auth_integration.py index a52d9493c..95c1880d3 100644 --- a/Server/tests/integration/test_middleware_auth_integration.py +++ b/Server/tests/integration/test_middleware_auth_integration.py @@ -57,17 +57,18 @@ async def test_sets_user_id_in_context_state(self, monkeypatch): middleware_ctx.fastmcp_context = ctx # Set an active instance so the middleware doesn't try to auto-select - middleware.set_active_instance(ctx, "Proj@hash1") + await middleware.set_active_instance(ctx, "Proj@hash1") # Register a matching session so resolution doesn't fail await registry.register("s1", "Proj", "hash1", "2022", user_id="user-55") await middleware._inject_unity_instance(middleware_ctx) - assert ctx.get_state("user_id") == "user-55" + assert await ctx.get_state("user_id") == "user-55" class TestMiddlewareSessionKey: - def test_get_session_key_uses_user_id_fallback(self): + @pytest.mark.asyncio + async def test_get_session_key_uses_user_id_fallback(self): """When no client_id, middleware should use user:$user_id as session key.""" from transport.unity_instance_middleware import UnityInstanceMiddleware @@ -77,12 +78,13 @@ def test_get_session_key_uses_user_id_fallback(self): # Simulate no client_id attribute if hasattr(ctx, "client_id"): delattr(ctx, "client_id") - ctx.set_state("user_id", "user-77") + await ctx.set_state("user_id", "user-77") - key = middleware.get_session_key(ctx) + key = await middleware.get_session_key(ctx) assert key == "user:user-77" - def test_get_session_key_prefers_client_id(self): + @pytest.mark.asyncio + async def test_get_session_key_prefers_client_id(self): """client_id should take precedence over user_id.""" from transport.unity_instance_middleware import UnityInstanceMiddleware @@ -90,9 +92,9 @@ def test_get_session_key_prefers_client_id(self): ctx = DummyContext() ctx.client_id = "client-abc" - ctx.set_state("user_id", "user-77") + await ctx.set_state("user_id", "user-77") - key = middleware.get_session_key(ctx) + key = await middleware.get_session_key(ctx) assert key == "client-abc" diff --git a/Server/tests/integration/test_multi_user_session_isolation.py b/Server/tests/integration/test_multi_user_session_isolation.py index e5b3bf670..ae64148d1 100644 --- a/Server/tests/integration/test_multi_user_session_isolation.py +++ b/Server/tests/integration/test_multi_user_session_isolation.py @@ -115,7 +115,7 @@ async def test_unity_instances_resource_filters_by_user(self, monkeypatch): from tests.integration.test_helpers import DummyContext ctx = DummyContext() - ctx.set_state("user_id", "userA") + await ctx.set_state("user_id", "userA") result = await unity_instances(ctx) @@ -145,11 +145,11 @@ async def test_set_active_instance_only_sees_own_sessions(self, monkeypatch): ) ctx = DummyContext() - ctx.set_state("user_id", "userA") + await ctx.set_state("user_id", "userA") result = await set_active_instance(ctx, "ProjectAlpha@hashA1") assert result["success"] is True - assert middleware.get_active_instance(ctx) == "ProjectAlpha@hashA1" + assert await middleware.get_active_instance(ctx) == "ProjectAlpha@hashA1" @pytest.mark.asyncio async def test_set_active_instance_rejects_other_users_instance(self, monkeypatch): @@ -169,7 +169,7 @@ async def test_set_active_instance_rejects_other_users_instance(self, monkeypatc ) ctx = DummyContext() - ctx.set_state("user_id", "userA") + await ctx.set_state("user_id", "userA") # UserA tries to select UserB's instance -> should fail result = await set_active_instance(ctx, "ProjectBeta@hashB1") diff --git a/Server/tests/integration/test_refresh_unity_retry_recovery.py b/Server/tests/integration/test_refresh_unity_retry_recovery.py index 01916e2f8..8a0a8c19d 100644 --- a/Server/tests/integration/test_refresh_unity_retry_recovery.py +++ b/Server/tests/integration/test_refresh_unity_retry_recovery.py @@ -16,7 +16,7 @@ async def test_refresh_unity_recovers_from_retry_disconnect(monkeypatch): from services.tools.refresh_unity import refresh_unity ctx = DummyContext() - ctx.set_state("unity_instance", "UnityMCPTests@cc8756d4cce0805a") + await ctx.set_state("unity_instance", "UnityMCPTests@cc8756d4cce0805a") # Seed dirty state inst = "UnityMCPTests@cc8756d4cce0805a" diff --git a/Server/tests/test_custom_tool_service_user_scope.py b/Server/tests/test_custom_tool_service_user_scope.py index a291199c5..3387fd4ca 100644 --- a/Server/tests/test_custom_tool_service_user_scope.py +++ b/Server/tests/test_custom_tool_service_user_scope.py @@ -68,7 +68,7 @@ async def test_execute_custom_tool_threads_user_id_from_context(monkeypatch): ctx = Mock() state = {"unity_instance": "Project@project-hash", "user_id": "user-1"} - ctx.get_state = Mock(side_effect=lambda key: state.get(key)) + ctx.get_state = AsyncMock(side_effect=lambda key: state.get(key)) service = Mock() service.execute_tool = AsyncMock(return_value=MCPResponse(success=True, message="ok")) @@ -92,7 +92,7 @@ async def test_custom_tools_resource_threads_user_id_from_context(monkeypatch): ctx = Mock() state = {"unity_instance": "Project@project-hash", "user_id": "user-1"} - ctx.get_state = Mock(side_effect=lambda key: state.get(key)) + ctx.get_state = AsyncMock(side_effect=lambda key: state.get(key)) service = Mock() service.list_registered_tools = AsyncMock( diff --git a/Server/tests/test_manage_animation.py b/Server/tests/test_manage_animation.py index 127a7a5f8..85c4f9651 100644 --- a/Server/tests/test_manage_animation.py +++ b/Server/tests/test_manage_animation.py @@ -3,7 +3,7 @@ import asyncio import json import pytest -from unittest.mock import patch, MagicMock +from unittest.mock import patch, MagicMock, AsyncMock from click.testing import CliRunner from cli.commands.animation import animation @@ -98,7 +98,7 @@ def test_unknown_action_returns_error(self): from services.tools.manage_animation import manage_animation ctx = MagicMock() - ctx.get_state = MagicMock(return_value=None) + ctx.get_state = AsyncMock(return_value=None) result = asyncio.run(manage_animation(ctx, action="invalid_action")) assert result["success"] is False @@ -108,7 +108,7 @@ def test_unknown_animator_action_suggests_prefix(self): from services.tools.manage_animation import manage_animation ctx = MagicMock() - ctx.get_state = MagicMock(return_value=None) + ctx.get_state = AsyncMock(return_value=None) result = asyncio.run(manage_animation(ctx, action="animator_nonexistent")) assert result["success"] is False @@ -118,7 +118,7 @@ def test_unknown_clip_action_suggests_prefix(self): from services.tools.manage_animation import manage_animation ctx = MagicMock() - ctx.get_state = MagicMock(return_value=None) + ctx.get_state = AsyncMock(return_value=None) result = asyncio.run(manage_animation(ctx, action="clip_nonexistent")) assert result["success"] is False @@ -128,7 +128,7 @@ def test_unknown_controller_action_suggests_prefix(self): from services.tools.manage_animation import manage_animation ctx = MagicMock() - ctx.get_state = MagicMock(return_value=None) + ctx.get_state = AsyncMock(return_value=None) result = asyncio.run(manage_animation(ctx, action="controller_nonexistent")) assert result["success"] is False @@ -138,7 +138,7 @@ def test_no_prefix_action_suggests_valid_prefixes(self): from services.tools.manage_animation import manage_animation ctx = MagicMock() - ctx.get_state = MagicMock(return_value=None) + ctx.get_state = AsyncMock(return_value=None) result = asyncio.run(manage_animation(ctx, action="bogus")) assert result["success"] is False diff --git a/Server/tests/test_manage_vfx_actions.py b/Server/tests/test_manage_vfx_actions.py index 8ae431411..f81936a75 100644 --- a/Server/tests/test_manage_vfx_actions.py +++ b/Server/tests/test_manage_vfx_actions.py @@ -2,6 +2,7 @@ import asyncio from types import SimpleNamespace +from unittest.mock import AsyncMock from services.tools.manage_vfx import manage_vfx @@ -17,7 +18,7 @@ async def fake_send_with_unity_instance(send_fn, unity_instance, tool_name, para monkeypatch.setattr( "services.tools.manage_vfx.get_unity_instance_from_context", - lambda _ctx: "unity-instance-1", + AsyncMock(return_value="unity-instance-1"), ) monkeypatch.setattr( "services.tools.manage_vfx.send_with_unity_instance", diff --git a/Server/tests/test_transport_characterization.py b/Server/tests/test_transport_characterization.py index 3b68677ac..f8b04b65e 100644 --- a/Server/tests/test_transport_characterization.py +++ b/Server/tests/test_transport_characterization.py @@ -60,8 +60,8 @@ def mock_context(): ctx.client_id = "test-client-456" state_storage = {} - ctx.set_state = Mock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) - ctx.get_state = Mock(side_effect=lambda k: state_storage.get(k)) + ctx.set_state = AsyncMock(side_effect=lambda k, v: state_storage.__setitem__(k, v)) + ctx.get_state = AsyncMock(side_effect=lambda k: state_storage.get(k)) ctx.info = AsyncMock() return ctx @@ -105,7 +105,8 @@ async def configured_plugin_hub(plugin_registry): class TestUnityInstanceMiddlewareSessionManagement: """Test instance routing and per-session state management.""" - def test_middleware_stores_instance_per_session(self, mock_context): + @pytest.mark.asyncio + async def test_middleware_stores_instance_per_session(self, mock_context): """ Current behavior: Middleware maintains independent instance selection per session using get_session_key() derivation. @@ -113,13 +114,14 @@ def test_middleware_stores_instance_per_session(self, mock_context): middleware = UnityInstanceMiddleware() instance_id = "TestProject@abc123def456" - middleware.set_active_instance(mock_context, instance_id) - retrieved = middleware.get_active_instance(mock_context) + await middleware.set_active_instance(mock_context, instance_id) + retrieved = await middleware.get_active_instance(mock_context) assert retrieved == instance_id, \ "Middleware must store and retrieve instance per session" - def test_middleware_uses_client_id_over_session_id(self): + @pytest.mark.asyncio + async def test_middleware_uses_client_id_over_session_id(self): """ Current behavior: get_session_key() prioritizes client_id for stability, falling back to 'global' when unavailable. @@ -130,10 +132,11 @@ def test_middleware_uses_client_id_over_session_id(self): ctx.client_id = "stable-client-id" ctx.session_id = "unstable-session-id" - key = middleware.get_session_key(ctx) + key = await middleware.get_session_key(ctx) assert key == "stable-client-id" - def test_middleware_falls_back_to_global_key(self): + @pytest.mark.asyncio + async def test_middleware_falls_back_to_global_key(self): """ Current behavior: When client_id is None/missing, use 'global' key. This allows single-user local mode to work without session tracking. @@ -143,11 +146,13 @@ def test_middleware_falls_back_to_global_key(self): ctx = Mock() ctx.client_id = None ctx.session_id = "session-id" + ctx.get_state = AsyncMock(return_value=None) - key = middleware.get_session_key(ctx) + key = await middleware.get_session_key(ctx) assert key == "global" - def test_middleware_isolates_multiple_sessions(self): + @pytest.mark.asyncio + async def test_middleware_isolates_multiple_sessions(self): """ Current behavior: Different sessions (different client_ids) maintain separate instance selections. @@ -162,13 +167,14 @@ def test_middleware_isolates_multiple_sessions(self): ctx2.client_id = "client-2" ctx2.session_id = "session-2" - middleware.set_active_instance(ctx1, "Project1@hash1") - middleware.set_active_instance(ctx2, "Project2@hash2") + await middleware.set_active_instance(ctx1, "Project1@hash1") + await middleware.set_active_instance(ctx2, "Project2@hash2") - assert middleware.get_active_instance(ctx1) == "Project1@hash1" - assert middleware.get_active_instance(ctx2) == "Project2@hash2" + assert await middleware.get_active_instance(ctx1) == "Project1@hash1" + assert await middleware.get_active_instance(ctx2) == "Project2@hash2" - def test_middleware_clear_instance(self, mock_context): + @pytest.mark.asyncio + async def test_middleware_clear_instance(self, mock_context): """ Current behavior: clear_active_instance() removes stored instance for the session, allowing reset to None. @@ -176,13 +182,14 @@ def test_middleware_clear_instance(self, mock_context): middleware = UnityInstanceMiddleware() instance_id = "TestProject@xyz" - middleware.set_active_instance(mock_context, instance_id) - assert middleware.get_active_instance(mock_context) == instance_id + await middleware.set_active_instance(mock_context, instance_id) + assert await middleware.get_active_instance(mock_context) == instance_id - middleware.clear_active_instance(mock_context) - assert middleware.get_active_instance(mock_context) is None + await middleware.clear_active_instance(mock_context) + assert await middleware.get_active_instance(mock_context) is None - def test_middleware_thread_safe_updates(self): + @pytest.mark.asyncio + async def test_middleware_thread_safe_updates(self): """ Current behavior: Middleware uses RLock to serialize access to _active_by_key dictionary. @@ -195,10 +202,10 @@ def test_middleware_thread_safe_updates(self): # Rapidly update instances (would race without locking) for i in range(10): instance = f"Project{i}@hash{i}" - middleware.set_active_instance(ctx, instance) + await middleware.set_active_instance(ctx, instance) # Final state should be consistent - assert middleware.get_active_instance(ctx) == "Project9@hash9" + assert await middleware.get_active_instance(ctx) == "Project9@hash9" # ============================================================================ @@ -218,7 +225,7 @@ async def test_middleware_injects_into_tool_context(self, mock_context): middleware = UnityInstanceMiddleware() instance_id = "Project@abc123" - middleware.set_active_instance(mock_context, instance_id) + await middleware.set_active_instance(mock_context, instance_id) # Create middleware context wrapper middleware_ctx = Mock() @@ -244,7 +251,7 @@ async def test_middleware_injects_into_resource_context(self, mock_context): middleware = UnityInstanceMiddleware() instance_id = "Project@hash123" - middleware.set_active_instance(mock_context, instance_id) + await middleware.set_active_instance(mock_context, instance_id) middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context @@ -292,7 +299,7 @@ async def test_list_tools_filters_disabled_unity_tools_and_aliases(self, mock_co middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("unity_instance", "Project@abc123") monkeypatch.setattr(config, "transport_mode", "http") available_tools = [ @@ -346,7 +353,7 @@ async def test_list_tools_skips_filter_when_no_tools_registered_yet(self, mock_c middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("unity_instance", "Project@abc123") monkeypatch.setattr(config, "transport_mode", "http") original_tools = [ @@ -399,7 +406,7 @@ async def test_list_tools_filters_when_all_tools_disabled(self, mock_context, mo middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("unity_instance", "Project@abc123") monkeypatch.setattr(config, "transport_mode", "http") original_tools = [ @@ -455,7 +462,7 @@ async def test_list_tools_skips_filter_when_enabled_set_lookup_fails(self, mock_ middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("unity_instance", "Project@abc123") monkeypatch.setattr(config, "transport_mode", "http") original_tools = [ @@ -498,8 +505,8 @@ async def test_list_tools_uses_user_scoped_tool_lookup_in_hosted_mode(self, mock middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") - mock_context.set_state("user_id", "user-123") + await mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("user_id", "user-123") monkeypatch.setattr(config, "transport_mode", "http") monkeypatch.setattr(config, "http_remote_hosted", True) @@ -534,7 +541,7 @@ async def test_list_tools_skips_filter_when_active_instance_hash_is_stale(self, middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@stale-hash") + await mock_context.set_state("unity_instance", "Project@stale-hash") monkeypatch.setattr(config, "transport_mode", "http") original_tools = [ @@ -573,7 +580,7 @@ async def test_list_tools_hides_alias_when_target_tool_is_disabled(self, mock_co middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("unity_instance", "Project@abc123") monkeypatch.setattr(config, "transport_mode", "http") original_tools = [ @@ -618,7 +625,7 @@ async def test_list_tools_keeps_all_visible_when_tool_registry_is_empty(self, mo middleware_ctx = Mock() middleware_ctx.fastmcp_context = mock_context - mock_context.set_state("unity_instance", "Project@abc123") + await mock_context.set_state("unity_instance", "Project@abc123") monkeypatch.setattr(config, "transport_mode", "http") original_tools = [ @@ -740,7 +747,7 @@ async def test_autoselect_via_plugin_hub_single_instance(self, mock_context): instance = await middleware._maybe_autoselect_instance(mock_context) assert instance == "TestProject@abc123" - assert middleware.get_active_instance(mock_context) == "TestProject@abc123" + assert await middleware.get_active_instance(mock_context) == "TestProject@abc123" @pytest.mark.asyncio async def test_autoselect_fails_with_multiple_instances(self, mock_context): @@ -1452,7 +1459,8 @@ async def test_middleware_handles_exception_during_autoselect(self, mock_context assert instance is None - def test_middleware_handles_client_id_false_but_not_none(self): + @pytest.mark.asyncio + async def test_middleware_handles_client_id_false_but_not_none(self): """ Current behavior: get_session_key checks isinstance(client_id, str) AND len, so falsy non-string values fall through to 'global'. @@ -1462,8 +1470,9 @@ def test_middleware_handles_client_id_false_but_not_none(self): ctx = Mock() ctx.client_id = "" # Empty string ctx.session_id = "session-id" + ctx.get_state = AsyncMock(return_value=None) - key = middleware.get_session_key(ctx) + key = await middleware.get_session_key(ctx) assert key == "global" # Empty string doesn't pass isinstance+truthy check def test_plugin_hub_encoding_is_json(self): @@ -1506,10 +1515,10 @@ async def test_middleware_and_registry_interaction(self, mock_context, plugin_re ) # Middleware stores the instance - middleware.set_active_instance(mock_context, "Project@hash-interact") + await middleware.set_active_instance(mock_context, "Project@hash-interact") # Application can use middleware to route - instance = middleware.get_active_instance(mock_context) + instance = await middleware.get_active_instance(mock_context) assert instance == "Project@hash-interact" # And registry to find session @@ -1534,10 +1543,10 @@ async def test_registry_and_middleware_complete_flow(self, mock_context, plugin_ ) # 2. User selects instance via middleware - middleware.set_active_instance(mock_context, "CompleteProject@hash-complete") + await middleware.set_active_instance(mock_context, "CompleteProject@hash-complete") # 3. Tools route using both middleware + registry - selected_instance = middleware.get_active_instance(mock_context) + selected_instance = await middleware.get_active_instance(mock_context) assert selected_instance == "CompleteProject@hash-complete" # Extract hash and resolve back to session diff --git a/Server/uv.lock b/Server/uv.lock index 995a01a3d..6d8807693 100644 --- a/Server/uv.lock +++ b/Server/uv.lock @@ -14,6 +14,18 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -46,15 +58,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, -] - [[package]] name = "attrs" version = "25.4.0" @@ -112,6 +115,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, ] +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -304,15 +326,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] -[[package]] -name = "cloudpickle" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -508,15 +521,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/5b/0eceb9a5990de9025733a0d212ca43649ba9facd58b8552b6bf93c11439d/cyclopts-4.4.4-py3-none-any.whl", hash = "sha256:316f798fe2f2a30cb70e7140cfde2a46617bfbb575d31bbfdc0b2410a447bd83", size = 197398, upload-time = "2026-01-05T03:40:17.141Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -569,25 +573,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] -[[package]] -name = "fakeredis" -version = "2.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "redis" }, - { name = "sortedcontainers" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, -] - -[package.optional-dependencies] -lua = [ - { name = "lupa" }, -] - [[package]] name = "fastapi" version = "0.128.0" @@ -605,29 +590,33 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.14.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonref" }, { name = "jsonschema-path" }, { name = "mcp" }, { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, { name = "platformdirs" }, - { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, - { name = "pydocket" }, { name = "pyperclip" }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "rich" }, { name = "uvicorn" }, + { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331, upload-time = "2025-12-15T02:26:27.102Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176, upload-time = "2025-12-15T02:26:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" }, ] [[package]] @@ -751,6 +740,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -811,80 +809,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] -[[package]] -name = "lupa" -version = "2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" }, - { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" }, - { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" }, - { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" }, - { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" }, - { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" }, - { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" }, - { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" }, - { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" }, - { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" }, - { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" }, - { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" }, - { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, - { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, - { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, - { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, - { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, - { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, - { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, - { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, - { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, - { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, - { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, - { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, - { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, - { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, - { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, - { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, - { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, - { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, - { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, - { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, - { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, - { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, - { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, - { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, - { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, - { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, - { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -948,7 +872,7 @@ dev = [ requires-dist = [ { name = "click", specifier = ">=8.1.0" }, { name = "fastapi", specifier = ">=0.104.0" }, - { name = "fastmcp", specifier = "==2.14.1" }, + { name = "fastmcp", specifier = ">=3.0.0,<4" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "mcp", specifier = ">=1.16.0" }, { name = "pydantic", specifier = ">=2.12.5" }, @@ -1003,62 +927,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] -[[package]] -name = "opentelemetry-exporter-prometheus" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "prometheus-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -1077,15 +945,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, ] -[[package]] -name = "pathvalidate" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, -] - [[package]] name = "platformdirs" version = "4.5.1" @@ -1104,32 +963,23 @@ 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 = "prometheus-client" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, -] - [[package]] name = "py-key-value-aio" -version = "0.3.0" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, - { name = "py-key-value-shared" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, ] [package.optional-dependencies] -disk = [ - { name = "diskcache" }, - { name = "pathvalidate" }, +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, ] keyring = [ { name = "keyring" }, @@ -1137,22 +987,6 @@ keyring = [ memory = [ { name = "cachetools" }, ] -redis = [ - { name = "redis" }, -] - -[[package]] -name = "py-key-value-shared" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beartype" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, -] [[package]] name = "pycparser" @@ -1315,30 +1149,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] -[[package]] -name = "pydocket" -version = "0.16.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cloudpickle" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "fakeredis", extra = ["lua"] }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-prometheus" }, - { name = "opentelemetry-instrumentation" }, - { name = "prometheus-client" }, - { name = "py-key-value-aio", extra = ["memory", "redis"] }, - { name = "python-json-logger" }, - { name = "redis" }, - { name = "rich" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080, upload-time = "2025-12-23T23:37:33.32Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087, upload-time = "2025-12-23T23:37:31.829Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -1426,15 +1236,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] -[[package]] -name = "python-json-logger" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, -] - [[package]] name = "python-multipart" version = "0.0.21" @@ -1539,18 +1340,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, -] - [[package]] name = "referencing" version = "0.36.2" @@ -1741,24 +1530,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] -[[package]] -name = "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 = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - [[package]] name = "sse-starlette" version = "3.1.2" @@ -1834,21 +1605,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] -[[package]] -name = "typer" -version = "0.21.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -1893,6 +1649,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + [[package]] name = "websockets" version = "15.0.1" @@ -1952,75 +1811,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - [[package]] name = "zipp" version = "3.23.0" From 89b0ddf3b395928284184ad24f3f34d1e945054b Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:24:59 -0500 Subject: [PATCH 2/7] clean up and doc update --- README.md | 4 ++-- Server/src/main.py | 15 --------------- Server/src/services/tools/__init__.py | 1 - Server/src/transport/unity_instance_middleware.py | 8 ++++---- docs/i18n/README-zh.md | 4 ++-- manifest.json | 4 ++++ 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e2ec7d876..4a017bf6d 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,10 @@ openupm add com.coplaydev.unity-mcp * **Extensible** — Works with various MCP Clients ### Available Tools -`apply_text_edits` • `batch_execute` • `create_script` • `debug_request_context` • `delete_script` • `execute_custom_tool` • `execute_menu_item` • `find_gameobjects` • `find_in_file` • `get_sha` • `get_test_job` • `manage_animation` • `manage_asset` • `manage_components` • `manage_editor` • `manage_gameobject` • `manage_material` • `manage_prefabs` • `manage_scene` • `manage_script` • `manage_script_capabilities` • `manage_scriptable_object` • `manage_shader` • `manage_texture` • `manage_ui` • `manage_vfx` • `read_console` • `refresh_unity` • `run_tests` • `script_apply_edits` • `set_active_instance` • `validate_script` +`apply_text_edits` • `batch_execute` • `create_script` • `debug_request_context` • `delete_script` • `execute_custom_tool` • `execute_menu_item` • `find_gameobjects` • `find_in_file` • `get_sha` • `get_test_job` • `manage_animation` • `manage_asset` • `manage_components` • `manage_editor` • `manage_gameobject` • `manage_material` • `manage_prefabs` • `manage_scene` • `manage_script` • `manage_script_capabilities` • `manage_scriptable_object` • `manage_shader` • `manage_texture` • `manage_tools` • `manage_ui` • `manage_vfx` • `read_console` • `refresh_unity` • `run_tests` • `script_apply_edits` • `set_active_instance` • `validate_script` ### Available Resources -`custom_tools` • `editor_active_tool` • `editor_prefab_stage` • `editor_selection` • `editor_state` • `editor_windows` • `gameobject` • `gameobject_api` • `gameobject_component` • `gameobject_components` • `get_tests` • `get_tests_for_mode` • `menu_items` • `prefab_api` • `prefab_hierarchy` • `prefab_info` • `project_info` • `project_layers` • `project_tags` • `unity_instances` +`custom_tools` • `editor_active_tool` • `editor_prefab_stage` • `editor_selection` • `editor_state` • `editor_windows` • `gameobject` • `gameobject_api` • `gameobject_component` • `gameobject_components` • `get_tests` • `get_tests_for_mode` • `menu_items` • `prefab_api` • `prefab_hierarchy` • `prefab_info` • `project_info` • `project_layers` • `project_tags` • `tool_groups` • `unity_instances` **Performance Tip:** Use `batch_execute` for multiple operations — it's 10-100x faster than individual calls! diff --git a/Server/src/main.py b/Server/src/main.py index a8d3486da..d8b2bcf34 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -347,21 +347,6 @@ async def health_http(_: Request) -> JSONResponse: "message": "MCP for Unity server is running" }) - @mcp.custom_route("/api/debug/list-tools", methods=["GET"]) - async def debug_list_tools(_: Request) -> JSONResponse: - """Diagnostic endpoint: returns registered tools and transform info (bypasses middleware).""" - try: - # Access the internal tool manager to avoid creating a dummy MCP context - tool_manager = mcp._tool_manager - all_tool_names = sorted(tool_manager.tools.keys()) if tool_manager else [] - return JSONResponse({ - "registered_count": len(all_tool_names), - "tools": all_tool_names, - "transform_count": len(mcp._transforms), - }) - except Exception as e: - return JSONResponse({"error": str(e)}, status_code=500) - @mcp.custom_route("/api/auth/login-url", methods=["GET"]) async def auth_login_url(_: Request) -> JSONResponse: """Return the login URL for users to obtain/manage API keys.""" diff --git a/Server/src/services/tools/__init__.py b/Server/src/services/tools/__init__.py index 8c452c6dd..3f3c6c049 100644 --- a/Server/src/services/tools/__init__.py +++ b/Server/src/services/tools/__init__.py @@ -77,7 +77,6 @@ def register_all_tools(mcp: FastMCP, *, project_scoped_tools: bool = True): logger.info( f"Default tool groups: {', '.join(sorted(DEFAULT_ENABLED_GROUPS))}. " f"Disabled: {', '.join(sorted(groups_to_disable))}. " - f"Transform count: {len(mcp._transforms)}. " "Use manage_tools to activate more." ) diff --git a/Server/src/transport/unity_instance_middleware.py b/Server/src/transport/unity_instance_middleware.py index 86db34cb3..a022e928e 100644 --- a/Server/src/transport/unity_instance_middleware.py +++ b/Server/src/transport/unity_instance_middleware.py @@ -432,19 +432,19 @@ async def on_list_tools(self, context: MiddlewareContext, call_next): tools = await call_next(context) tool_names_from_fastmcp = sorted(getattr(t, "name", "?") for t in tools) - _diag.info( + _diag.debug( "on_list_tools: FastMCP returned %d tools: %s", len(tools), tool_names_from_fastmcp, ) if not self._should_filter_tool_listing(): - _diag.info("on_list_tools: skipping middleware filter (not HTTP or PluginHub not configured)") + _diag.debug("on_list_tools: skipping middleware filter (not HTTP or PluginHub not configured)") return tools self._refresh_tool_visibility_metadata_from_registry() enabled_tool_names = await self._resolve_enabled_tool_names_for_context(context) if enabled_tool_names is None: - _diag.info("on_list_tools: no Unity session data, returning %d tools from FastMCP as-is", len(tools)) + _diag.debug("on_list_tools: no Unity session data, returning %d tools from FastMCP as-is", len(tools)) return tools filtered = [] @@ -453,7 +453,7 @@ async def on_list_tools(self, context: MiddlewareContext, call_next): if self._is_tool_visible(tool_name, enabled_tool_names): filtered.append(tool) - _diag.info( + _diag.debug( "on_list_tools: filtered %d/%d tools visible (Unity register_tools). " "enabled_names=%s", len(filtered), len(tools), sorted(enabled_tool_names), diff --git a/docs/i18n/README-zh.md b/docs/i18n/README-zh.md index f37603b90..c7b731c8a 100644 --- a/docs/i18n/README-zh.md +++ b/docs/i18n/README-zh.md @@ -76,10 +76,10 @@ openupm add com.coplaydev.unity-mcp * **可扩展** — 可与多种 MCP Client 配合使用 ### 可用工具 -`apply_text_edits` • `batch_execute` • `create_script` • `debug_request_context` • `delete_script` • `execute_custom_tool` • `execute_menu_item` • `find_gameobjects` • `find_in_file` • `get_sha` • `get_test_job` • `manage_animation` • `manage_asset` • `manage_components` • `manage_editor` • `manage_gameobject` • `manage_material` • `manage_prefabs` • `manage_scene` • `manage_script` • `manage_script_capabilities` • `manage_scriptable_object` • `manage_shader` • `manage_texture` • `manage_ui` • `manage_vfx` • `read_console` • `refresh_unity` • `run_tests` • `script_apply_edits` • `set_active_instance` • `validate_script` +`apply_text_edits` • `batch_execute` • `create_script` • `debug_request_context` • `delete_script` • `execute_custom_tool` • `execute_menu_item` • `find_gameobjects` • `find_in_file` • `get_sha` • `get_test_job` • `manage_animation` • `manage_asset` • `manage_components` • `manage_editor` • `manage_gameobject` • `manage_material` • `manage_prefabs` • `manage_scene` • `manage_script` • `manage_script_capabilities` • `manage_scriptable_object` • `manage_shader` • `manage_texture` • `manage_tools` • `manage_ui` • `manage_vfx` • `read_console` • `refresh_unity` • `run_tests` • `script_apply_edits` • `set_active_instance` • `validate_script` ### 可用资源 -`custom_tools` • `editor_active_tool` • `editor_prefab_stage` • `editor_selection` • `editor_state` • `editor_windows` • `gameobject` • `gameobject_api` • `gameobject_component` • `gameobject_components` • `get_tests` • `get_tests_for_mode` • `menu_items` • `prefab_api` • `prefab_hierarchy` • `prefab_info` • `project_info` • `project_layers` • `project_tags` • `unity_instances` +`custom_tools` • `editor_active_tool` • `editor_prefab_stage` • `editor_selection` • `editor_state` • `editor_windows` • `gameobject` • `gameobject_api` • `gameobject_component` • `gameobject_components` • `get_tests` • `get_tests_for_mode` • `menu_items` • `prefab_api` • `prefab_hierarchy` • `prefab_info` • `project_info` • `project_layers` • `project_tags` • `tool_groups` • `unity_instances` **性能提示:** 多个操作请使用 `batch_execute` — 比逐个调用快 10-100 倍! diff --git a/manifest.json b/manifest.json index 7fba1e444..fec16c3cd 100644 --- a/manifest.json +++ b/manifest.json @@ -125,6 +125,10 @@ "name": "manage_texture", "description": "Create and modify textures with patterns, gradients, and noise" }, + { + "name": "manage_tools", + "description": "Manage which tool groups are visible in this session" + }, { "name": "manage_ui", "description": "Manage Unity UI Toolkit elements (UXML documents, USS stylesheets, UIDocument components)" From e05d06681e38019f09d7476b7b054596e9f252ee Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:10:55 -0500 Subject: [PATCH 3/7] Update for Stdio mode --- .../Clients/McpClientConfiguratorBase.cs | 200 ++++++++++++++++-- .../Editor/Constants/EditorPrefKeys.cs | 1 + MCPForUnity/Editor/Helpers/ExecPath.cs | 9 +- .../Editor/Resources/Editor/ToolStates.cs | 57 +++++ .../Resources/Editor/ToolStates.cs.meta | 11 + .../Editor/Services/PathResolverService.cs | 8 +- .../ClientConfig/McpClientConfigSection.cs | 79 ++++++- .../ClientConfig/McpClientConfigSection.uxml | 6 + .../Connection/McpConnectionSection.cs | 10 + .../Components/Tools/McpToolsSection.cs | 2 +- Server/src/main.py | 25 +++ Server/src/services/tools/__init__.py | 141 +++++++++++- Server/src/services/tools/manage_tools.py | 34 ++- Server/src/transport/plugin_hub.py | 61 ++++++ 14 files changed, 603 insertions(+), 41 deletions(-) create mode 100644 MCPForUnity/Editor/Resources/Editor/ToolStates.cs create mode 100644 MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index f2f170736..7b7f9b8cd 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -543,13 +543,39 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => "Managed via Claude CLI"; /// + /// Returns the project directory that CLI-based configurators will use as the working directory + /// for `claude mcp add/remove --scope local`. Checks for an explicit override in EditorPrefs + /// first, then falls back to the current Unity project directory. + /// The override is useful when the Claude Code workspace is at a different path than the Unity project + /// (e.g., plugin developers running CC from the repo root while Unity is open with a test project). + /// MUST be called from the main Unity thread (accesses Application.dataPath and EditorPrefs). + /// + internal static string GetClientProjectDir() + { + string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty); + if (!string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir)) + return overrideDir; + return Path.GetDirectoryName(Application.dataPath); + } + + /// + /// Returns true if a valid client project directory override is set. + /// + internal static bool HasClientProjectDirOverride + { + get + { + string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty); + return !string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir); + } + } /// Checks the Claude CLI registration status. /// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access. /// public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { // Capture main-thread-only values before delegating to thread-safe method - string projectDir = Path.GetDirectoryName(Application.dataPath); + string projectDir = GetClientProjectDir(); bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; // Resolve claudePath on the main thread (EditorPrefs access) string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); @@ -557,7 +583,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) bool isRemoteScope = HttpEndpointUtility.IsRemoteScope(); // Get expected package source for the installed package version (matches what Register() would use) string expectedPackageSource = GetExpectedPackageSourceForValidation(); - return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite); + return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite, HasClientProjectDirOverride); } /// @@ -572,7 +598,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) internal McpStatus CheckStatusWithProjectDir( string projectDir, bool useHttpTransport, string claudePath, RuntimePlatform platform, bool isRemoteScope, string expectedPackageSource, - bool attemptAutoRewrite = false) + bool attemptAutoRewrite = false, bool hasProjectDirOverride = false) { try { @@ -632,8 +658,12 @@ internal McpStatus CheckStatusWithProjectDir( client.configuredTransport = Models.ConfiguredTransport.Unknown; } - // Check for transport mismatch - bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp); + // Check for transport mismatch. + // When a project dir override is active, the local UseHttpTransport + // GUI setting may legitimately differ from the registered transport + // in the overridden project, so skip this check. + bool hasTransportMismatch = !hasProjectDirOverride + && ((currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp)); // For stdio transport, also check package version bool hasVersionMismatch = false; @@ -873,7 +903,7 @@ private void Register() args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}"; } - string projectDir = Path.GetDirectoryName(Application.dataPath); + string projectDir = GetClientProjectDir(); string pathPrepend = null; if (Application.platform == RuntimePlatform.OSXEditor) @@ -925,7 +955,7 @@ private void Unregister() throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); } - string projectDir = Path.GetDirectoryName(Application.dataPath); + string projectDir = GetClientProjectDir(); string pathPrepend = null; if (Application.platform == RuntimePlatform.OSXEditor) { @@ -997,6 +1027,7 @@ public override string GetManualSnippet() /// /// Removes UnityMCP registration from all Claude Code configuration scopes (local, user, project). + /// Also removes legacy entries from ~/.claude.json that the CLI scoped removal can't touch. /// This ensures no stale or conflicting configurations remain across different scopes. /// Also handles legacy "unityMCP" naming convention. /// @@ -1015,6 +1046,81 @@ private static void RemoveFromAllScopes(string claudePath, string projectDir, st ExecPath.TryRun(claudePath, $"mcp remove --scope {scope} {name}", projectDir, out _, out _, 5000, pathPrepend); } } + + // Also remove legacy entries directly from ~/.claude.json. + // Older versions and manual CLI commands without --scope wrote mcpServers entries + // into the projects section of ~/.claude.json. The scoped `claude mcp remove` commands + // above won't touch these, leaving stale/conflicting configs behind. + RemoveLegacyUserConfigEntries(projectDir); + } + + /// + /// Removes UnityMCP entries from the projects section of ~/.claude.json. + /// These are legacy entries that were created by older versions or manual commands + /// that didn't use --scope. The scoped `claude mcp remove` commands don't clean these up. + /// + private static void RemoveLegacyUserConfigEntries(string projectDir) + { + try + { + string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string configPath = Path.Combine(homeDir, ".claude.json"); + if (!File.Exists(configPath)) + return; + + string json = File.ReadAllText(configPath); + var config = JObject.Parse(json); + var projects = config["projects"] as JObject; + if (projects == null) + return; + + string normalizedProjectDir = NormalizePath(projectDir); + bool modified = false; + + // Walk all project entries looking for ones that match our project path + foreach (var project in projects.Properties()) + { + string normalizedKey = NormalizePath(project.Name); + + // Match exact path or parent paths (same logic as ReadUserScopeConfig) + if (!string.Equals(normalizedKey, normalizedProjectDir, StringComparison.OrdinalIgnoreCase)) + { + // Also check if projectDir is a child of this config entry + if (!normalizedProjectDir.StartsWith(normalizedKey + "/", StringComparison.OrdinalIgnoreCase)) + continue; + } + + var mcpServers = project.Value?["mcpServers"] as JObject; + if (mcpServers == null) + continue; + + // Remove UnityMCP/unityMCP entries (case-insensitive) + var toRemove = new List(); + foreach (var server in mcpServers.Properties()) + { + if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase)) + { + toRemove.Add(server.Name); + } + } + + foreach (var name in toRemove) + { + mcpServers.Remove(name); + modified = true; + McpLog.Info($"Removed legacy '{name}' entry from ~/.claude.json for project '{project.Name}'"); + } + } + + if (modified) + { + File.WriteAllText(configPath, config.ToString(Formatting.Indented)); + } + } + catch (Exception ex) + { + McpLog.Warn($"Failed to clean up legacy ~/.claude.json entries: {ex.Message}"); + } } /// @@ -1095,32 +1201,89 @@ private static string ExtractPackageSourceFromCliOutput(string cliOutput) } /// - /// Reads Claude Code configuration directly from ~/.claude.json file. + /// Reads Claude Code configuration from both local-scope (.claude/mcp.json in the project) + /// and user-scope (~/.claude.json). Local scope takes precedence, matching Claude Code's + /// own config resolution order. /// This is much faster than running `claude mcp list` which does health checks on all servers. /// private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string projectDir) { try { - // Find the Claude config file + // 1. Check local-scope config first: {projectDir}/.claude/mcp.json + // This is where `claude mcp add --scope local` writes. + var localResult = ReadLocalScopeConfig(projectDir); + if (localResult.serverConfig != null) + return localResult; + if (localResult.error != null) + return localResult; + + // 2. Fall back to user-scope config: ~/.claude.json + return ReadUserScopeConfig(projectDir); + } + catch (Exception ex) + { + return (null, $"Error reading Claude config: {ex.Message}"); + } + } + + /// + /// Reads UnityMCP config from the local-scope file: {projectDir}/.claude/mcp.json. + /// This is where `claude mcp add --scope local` stores registrations. + /// + private static (JObject serverConfig, string error) ReadLocalScopeConfig(string projectDir) + { + try + { + if (string.IsNullOrEmpty(projectDir)) + return (null, null); + + string localConfigPath = Path.Combine(projectDir, ".claude", "mcp.json"); + if (!File.Exists(localConfigPath)) + return (null, null); + + string json = File.ReadAllText(localConfigPath); + var config = JObject.Parse(json); + var mcpServers = config["mcpServers"] as JObject; + if (mcpServers == null) + return (null, null); + + foreach (var server in mcpServers.Properties()) + { + if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase)) + { + return (server.Value as JObject, null); + } + } + + return (null, null); + } + catch (Exception ex) + { + return (null, $"Error reading local Claude config: {ex.Message}"); + } + } + + /// + /// Reads UnityMCP config from the user-scope file: ~/.claude.json (projects section). + /// This handles legacy configurations and direct user-level entries. + /// + private static (JObject serverConfig, string error) ReadUserScopeConfig(string projectDir) + { + try + { string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string configPath = Path.Combine(homeDir, ".claude.json"); if (!File.Exists(configPath)) - { - // Missing config file is "not configured", not an error - // (Claude Code may not be installed or just hasn't been configured yet) return (null, null); - } string configJson = File.ReadAllText(configPath); var config = JObject.Parse(configJson); var projects = config["projects"] as JObject; if (projects == null) - { - return (null, null); // No projects configured - } + return (null, null); // Build a dictionary of normalized paths for quick lookup // Use last entry for duplicates (forward/backslash variants) as it's typically more recent @@ -1142,7 +1305,6 @@ private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string var mcpServers = projectConfig?["mcpServers"] as JObject; if (mcpServers != null) { - // Look for UnityMCP (case-insensitive) foreach (var server in mcpServers.Properties()) { if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase)) @@ -1162,11 +1324,11 @@ private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string currentDir = currentDir.Substring(0, lastSlash); } - return (null, null); // Project not found in config + return (null, null); } catch (Exception ex) { - return (null, $"Error reading Claude config: {ex.Message}"); + return (null, $"Error reading user Claude config: {ex.Message}"); } } diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index c5dab7c41..a7e150474 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -22,6 +22,7 @@ internal static class EditorPrefKeys internal const string UvxPathOverride = "MCPForUnity.UvxPath"; internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath"; + internal const string ClientProjectDirOverride = "MCPForUnity.ClientProjectDir"; internal const string HttpBaseUrl = "MCPForUnity.HttpUrl"; internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl"; diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs b/MCPForUnity/Editor/Helpers/ExecPath.cs index 3801a03a7..350689821 100644 --- a/MCPForUnity/Editor/Helpers/ExecPath.cs +++ b/MCPForUnity/Editor/Helpers/ExecPath.cs @@ -52,9 +52,14 @@ internal static string ResolveClaude() // Common npm global locations string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; string[] candidates = { - // Prefer .cmd (most reliable from non-interactive processes) + // Native installer locations + Path.Combine(localAppData, "Programs", "claude", "claude.exe"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"), + Path.Combine(home, ".local", "bin", "claude.exe"), + // npm global locations (.cmd preferred for non-interactive processes) Path.Combine(appData, "npm", "claude.cmd"), Path.Combine(localAppData, "npm", "claude.cmd"), // Fall back to PowerShell shim if only .ps1 is present @@ -277,7 +282,7 @@ private static string Which(string exe, string prependPath) #endif #if UNITY_EDITOR_WIN - private static string FindInPathWindows(string exe, string extraPathPrepend = null) + internal static string FindInPathWindows(string exe, string extraPathPrepend = null) { try { diff --git a/MCPForUnity/Editor/Resources/Editor/ToolStates.cs b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs new file mode 100644 index 000000000..9ca626121 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using Newtonsoft.Json.Linq; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Returns the enabled/disabled state of all discovered tools, grouped by group name. + /// Used by the Python server (especially in stdio mode) to sync tool visibility. + /// + [McpForUnityResource("get_tool_states")] + public static class ToolStates + { + public static object HandleCommand(JObject @params) + { + try + { + var discovery = MCPServiceLocator.ToolDiscovery; + var allTools = discovery.DiscoverAllTools(); + + var toolsArray = new JArray(); + foreach (var tool in allTools) + { + toolsArray.Add(new JObject + { + ["name"] = tool.Name, + ["group"] = tool.Group ?? "core", + ["enabled"] = discovery.IsToolEnabled(tool.Name) + }); + } + + var groups = allTools + .GroupBy(t => t.Group ?? "core") + .Select(g => new JObject + { + ["name"] = g.Key, + ["enabled_count"] = g.Count(t => discovery.IsToolEnabled(t.Name)), + ["total_count"] = g.Count() + }); + + var result = new JObject + { + ["tools"] = toolsArray, + ["groups"] = new JArray(groups) + }; + + return new SuccessResponse("Retrieved tool states.", result); + } + catch (Exception e) + { + return new ErrorResponse($"Failed to retrieve tool states: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta new file mode 100644 index 000000000..25da1749e --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0f77d36b37ba4526ad30b3c84e3e752c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs index 4181cf4c6..bf857a83c 100644 --- a/MCPForUnity/Editor/Services/PathResolverService.cs +++ b/MCPForUnity/Editor/Services/PathResolverService.cs @@ -114,13 +114,19 @@ public string GetClaudeCliPath() { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "claude", "claude.exe"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"), - "claude.exe" + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude.exe"), }; foreach (var c in candidates) { if (File.Exists(c)) return c; } + +#if UNITY_EDITOR_WIN + // Fall back to PATH search (handles non-standard install locations) + string fromPath = ExecPath.FindInPathWindows("claude.exe"); + if (!string.IsNullOrEmpty(fromPath)) return fromPath; +#endif } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index df2aa1781..294a50f94 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -33,6 +33,10 @@ public class McpClientConfigSection private VisualElement claudeCliPathRow; private TextField claudeCliPath; private Button browseClaudeButton; + private VisualElement clientProjectDirRow; + private TextField clientProjectDirField; + private Button browseProjectDirButton; + private Button clearProjectDirButton; private Foldout manualConfigFoldout; private TextField configPathField; private Button copyPathButton; @@ -84,6 +88,10 @@ private void CacheUIElements() claudeCliPathRow = Root.Q("claude-cli-path-row"); claudeCliPath = Root.Q("claude-cli-path"); browseClaudeButton = Root.Q