diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000..8364d1ba --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,45 @@ +name: Python Tests + +on: + push: + branches: ["**"] + paths: + - MCPForUnity/UnityMcpServer~/src/** + - .github/workflows/python-tests.yml + workflow_dispatch: {} + +jobs: + test: + name: Run Python Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python + run: uv python install 3.10 + + - name: Install dependencies + run: | + cd MCPForUnity/UnityMcpServer~/src + uv sync + uv pip install -e ".[dev]" + + - name: Run tests + run: | + cd MCPForUnity/UnityMcpServer~/src + uv run pytest tests/ -v --tb=short + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: pytest-results + path: | + MCPForUnity/UnityMcpServer~/src/.pytest_cache/ + MCPForUnity/UnityMcpServer~/src/tests/ diff --git a/.github/workflows/unity-tests.yml b/.github/workflows/unity-tests.yml index bfd04055..954fff30 100644 --- a/.github/workflows/unity-tests.yml +++ b/.github/workflows/unity-tests.yml @@ -3,7 +3,7 @@ name: Unity Tests on: workflow_dispatch: {} push: - branches: [main] + branches: ["**"] paths: - TestProjects/UnityMCPTests/** - MCPForUnity/Editor/** diff --git a/TestProjects/UnityMCPTests/Assets/Materials.meta b/MCPForUnity/Editor/Resources/Editor.meta similarity index 77% rename from TestProjects/UnityMCPTests/Assets/Materials.meta rename to MCPForUnity/Editor/Resources/Editor.meta index 7ad588cf..5c252d17 100644 --- a/TestProjects/UnityMCPTests/Assets/Materials.meta +++ b/MCPForUnity/Editor/Resources/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: bacdb2f03a45d448888245e6ac9cca1b +guid: 266967ec2e1df44209bf46ec6037d61d folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs b/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs new file mode 100644 index 00000000..0a3fa860 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs @@ -0,0 +1,64 @@ +using System; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Provides information about the currently active editor tool. + /// + [McpForUnityResource("get_active_tool")] + public static class ActiveTool + { + public static object HandleCommand(JObject @params) + { + try + { + Tool currentTool = UnityEditor.Tools.current; + string toolName = currentTool.ToString(); + bool customToolActive = UnityEditor.Tools.current == Tool.Custom; + string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; + + var toolInfo = new + { + activeTool = activeToolName, + isCustom = customToolActive, + pivotMode = UnityEditor.Tools.pivotMode.ToString(), + pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), + handleRotation = new + { + x = UnityEditor.Tools.handleRotation.eulerAngles.x, + y = UnityEditor.Tools.handleRotation.eulerAngles.y, + z = UnityEditor.Tools.handleRotation.eulerAngles.z + }, + handlePosition = new + { + x = UnityEditor.Tools.handlePosition.x, + y = UnityEditor.Tools.handlePosition.y, + z = UnityEditor.Tools.handlePosition.z + } + }; + + return Response.Success("Retrieved active tool information.", toolInfo); + } + catch (Exception e) + { + return Response.Error($"Error getting active tool: {e.Message}"); + } + } + } + + // Helper class for custom tool names + internal static class EditorTools + { + public static string GetActiveToolName() + { + if (UnityEditor.Tools.current == Tool.Custom) + { + return "Unknown Custom Tool"; + } + return UnityEditor.Tools.current.ToString(); + } + } +} diff --git a/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta b/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta new file mode 100644 index 00000000..a2f03abd --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e78b6227ab7742a8a4f679ee6a8a212 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Editor/EditorState.cs b/MCPForUnity/Editor/Resources/Editor/EditorState.cs new file mode 100644 index 00000000..fdcff7e6 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/EditorState.cs @@ -0,0 +1,40 @@ +using System; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Provides dynamic editor state information that changes frequently. + /// + [McpForUnityResource("get_editor_state")] + public static class EditorState + { + public static object HandleCommand(JObject @params) + { + try + { + var activeScene = EditorSceneManager.GetActiveScene(); + var state = new + { + isPlaying = EditorApplication.isPlaying, + isPaused = EditorApplication.isPaused, + isCompiling = EditorApplication.isCompiling, + isUpdating = EditorApplication.isUpdating, + timeSinceStartup = EditorApplication.timeSinceStartup, + activeSceneName = activeScene.name ?? "", + selectionCount = UnityEditor.Selection.count, + activeObjectName = UnityEditor.Selection.activeObject?.name + }; + + return Response.Success("Retrieved editor state.", state); + } + catch (Exception e) + { + return Response.Error($"Error getting editor state: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta b/MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta new file mode 100644 index 00000000..c6c5efa1 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f7c6df54e014c44fdb0cd3f65a479e37 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Editor/PrefabStage.cs b/MCPForUnity/Editor/Resources/Editor/PrefabStage.cs new file mode 100644 index 00000000..2f66a01f --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/PrefabStage.cs @@ -0,0 +1,42 @@ +using System; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor.SceneManagement; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Provides information about the current prefab editing context. + /// + [McpForUnityResource("get_prefab_stage")] + public static class PrefabStage + { + public static object HandleCommand(JObject @params) + { + try + { + var stage = PrefabStageUtility.GetCurrentPrefabStage(); + + if (stage == null) + { + return Response.Success("No prefab stage is currently open.", new { isOpen = false }); + } + + var stageInfo = new + { + isOpen = true, + assetPath = stage.assetPath, + prefabRootName = stage.prefabContentsRoot?.name, + mode = stage.mode.ToString(), + isDirty = stage.scene.isDirty + }; + + return Response.Success("Prefab stage info retrieved.", stageInfo); + } + catch (Exception e) + { + return Response.Error($"Error getting prefab stage info: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Editor/PrefabStage.cs.meta b/MCPForUnity/Editor/Resources/Editor/PrefabStage.cs.meta new file mode 100644 index 00000000..31bc264c --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/PrefabStage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a30b083e68bd4ae3b3d1ce5a45a9414 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Editor/Selection.cs b/MCPForUnity/Editor/Resources/Editor/Selection.cs new file mode 100644 index 00000000..07bb34d8 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/Selection.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Provides detailed information about the current editor selection. + /// + [McpForUnityResource("get_selection")] + public static class Selection + { + public static object HandleCommand(JObject @params) + { + try + { + var selectionInfo = new + { + activeObject = UnityEditor.Selection.activeObject?.name, + activeGameObject = UnityEditor.Selection.activeGameObject?.name, + activeTransform = UnityEditor.Selection.activeTransform?.name, + activeInstanceID = UnityEditor.Selection.activeInstanceID, + count = UnityEditor.Selection.count, + objects = UnityEditor.Selection.objects + .Select(obj => new + { + name = obj?.name, + type = obj?.GetType().FullName, + instanceID = obj?.GetInstanceID() + }) + .ToList(), + gameObjects = UnityEditor.Selection.gameObjects + .Select(go => new + { + name = go?.name, + instanceID = go?.GetInstanceID() + }) + .ToList(), + assetGUIDs = UnityEditor.Selection.assetGUIDs + }; + + return Response.Success("Retrieved current selection details.", selectionInfo); + } + catch (Exception e) + { + return Response.Error($"Error getting selection: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Editor/Selection.cs.meta b/MCPForUnity/Editor/Resources/Editor/Selection.cs.meta new file mode 100644 index 00000000..2066f11a --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/Selection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7ea869623e094599a70be086ab4fc0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Editor/Windows.cs b/MCPForUnity/Editor/Resources/Editor/Windows.cs new file mode 100644 index 00000000..a637c1e2 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/Windows.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Resources.Editor +{ + /// + /// Provides list of all open editor windows. + /// + [McpForUnityResource("get_windows")] + public static class Windows + { + public static object HandleCommand(JObject @params) + { + try + { + EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll(); + var openWindows = new List(); + + foreach (EditorWindow window in allWindows) + { + if (window == null) + continue; + + try + { + openWindows.Add(new + { + title = window.titleContent.text, + typeName = window.GetType().FullName, + isFocused = EditorWindow.focusedWindow == window, + position = new + { + x = window.position.x, + y = window.position.y, + width = window.position.width, + height = window.position.height + }, + instanceID = window.GetInstanceID() + }); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not get info for window {window.GetType().Name}: {ex.Message}"); + } + } + + return Response.Success("Retrieved list of open editor windows.", openWindows); + } + catch (Exception e) + { + return Response.Error($"Error getting editor windows: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Editor/Windows.cs.meta b/MCPForUnity/Editor/Resources/Editor/Windows.cs.meta new file mode 100644 index 00000000..57dd9edc --- /dev/null +++ b/MCPForUnity/Editor/Resources/Editor/Windows.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 58a341e64bea440b29deaf859aaea552 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Project.meta b/MCPForUnity/Editor/Resources/Project.meta new file mode 100644 index 00000000..1adf0443 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 538489f13d7914c4eba9a67e29001b43 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Project/Layers.cs b/MCPForUnity/Editor/Resources/Project/Layers.cs new file mode 100644 index 00000000..eb7f1a30 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project/Layers.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace MCPForUnity.Editor.Resources.Project +{ + /// + /// Provides dictionary of layer indices to layer names. + /// + [McpForUnityResource("get_layers")] + public static class Layers + { + private const int TotalLayerCount = 32; + + public static object HandleCommand(JObject @params) + { + try + { + var layers = new Dictionary(); + for (int i = 0; i < TotalLayerCount; i++) + { + string layerName = LayerMask.LayerToName(i); + if (!string.IsNullOrEmpty(layerName)) + { + layers.Add(i, layerName); + } + } + + return Response.Success("Retrieved current named layers.", layers); + } + catch (Exception e) + { + return Response.Error($"Failed to retrieve layers: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Project/Layers.cs.meta b/MCPForUnity/Editor/Resources/Project/Layers.cs.meta new file mode 100644 index 00000000..427a7e92 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project/Layers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 959ee428299454ac19a636275208ca00 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs b/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs new file mode 100644 index 00000000..33069831 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Resources.Project +{ + /// + /// Provides static project configuration information. + /// + [McpForUnityResource("get_project_info")] + public static class ProjectInfo + { + public static object HandleCommand(JObject @params) + { + try + { + string assetsPath = Application.dataPath.Replace('\\', '/'); + string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); + string projectName = Path.GetFileName(projectRoot); + + var info = new + { + projectRoot = projectRoot ?? "", + projectName = projectName ?? "", + unityVersion = Application.unityVersion, + platform = EditorUserBuildSettings.activeBuildTarget.ToString(), + assetsPath = assetsPath + }; + + return Response.Success("Retrieved project info.", info); + } + catch (Exception e) + { + return Response.Error($"Error getting project info: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs.meta b/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs.meta new file mode 100644 index 00000000..a8eaf02a --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 81b03415fcf93466e9ed667d19b58d43 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Project/Tags.cs b/MCPForUnity/Editor/Resources/Project/Tags.cs new file mode 100644 index 00000000..665e8d77 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project/Tags.cs @@ -0,0 +1,27 @@ +using System; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditorInternal; + +namespace MCPForUnity.Editor.Resources.Project +{ + /// + /// Provides list of all tags in the project. + /// + [McpForUnityResource("get_tags")] + public static class Tags + { + public static object HandleCommand(JObject @params) + { + try + { + string[] tags = InternalEditorUtility.tags; + return Response.Success("Retrieved current tags.", tags); + } + catch (Exception e) + { + return Response.Error($"Failed to retrieve tags: {e.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Resources/Project/Tags.cs.meta b/MCPForUnity/Editor/Resources/Project/Tags.cs.meta new file mode 100644 index 00000000..3529bea6 --- /dev/null +++ b/MCPForUnity/Editor/Resources/Project/Tags.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2179ac5d98f264d1681e7d5c0d0ed341 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/MCPForUnity/Editor/Resources/Tests/GetTests.cs index 07a233ab..3efb1c6b 100644 --- a/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ b/MCPForUnity/Editor/Resources/Tests/GetTests.cs @@ -87,19 +87,19 @@ internal static bool TryParse(string modeStr, out TestMode? mode, out string err return false; } - if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase)) + if (modeStr.Equals("EditMode", StringComparison.OrdinalIgnoreCase)) { mode = TestMode.EditMode; return true; } - if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase)) + if (modeStr.Equals("PlayMode", StringComparison.OrdinalIgnoreCase)) { mode = TestMode.PlayMode; return true; } - error = $"Unknown test mode: '{modeStr}'. Use 'edit' or 'play'"; + error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'"; return false; } } diff --git a/MCPForUnity/Editor/Tools/ManageEditor.cs b/MCPForUnity/Editor/Tools/ManageEditor.cs index 97a20a4e..87e4186f 100644 --- a/MCPForUnity/Editor/Tools/ManageEditor.cs +++ b/MCPForUnity/Editor/Tools/ManageEditor.cs @@ -1,19 +1,14 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.IO; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditorInternal; // Required for tag management -using UnityEditor.SceneManagement; -using UnityEngine; using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools { /// - /// Handles operations related to controlling and querying the Unity Editor state, - /// including managing Tags and Layers. + /// Handles editor control actions including play mode control, tool selection, + /// and tag/layer management. For reading editor state, use MCP resources instead. /// [McpForUnityTool("manage_editor")] public static class ManageEditor @@ -89,19 +84,7 @@ public static object HandleCommand(JObject @params) return Response.Error($"Error stopping play mode: {e.Message}"); } - // Editor State/Info - case "get_state": - return GetEditorState(); - case "get_project_root": - return GetProjectRoot(); - case "get_windows": - return GetEditorWindows(); - case "get_active_tool": - return GetActiveTool(); - case "get_selection": - return GetSelection(); - case "get_prefab_stage": - return GetPrefabStageInfo(); + // Tool Control case "set_active_tool": string toolName = @params["toolName"]?.ToString(); if (string.IsNullOrEmpty(toolName)) @@ -117,9 +100,6 @@ public static object HandleCommand(JObject @params) if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for remove_tag."); return RemoveTag(tagName); - case "get_tags": - return GetTags(); // Helper to list current tags - // Layer Management case "add_layer": if (string.IsNullOrEmpty(layerName)) @@ -129,9 +109,6 @@ public static object HandleCommand(JObject @params) if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for remove_layer."); return RemoveLayer(layerName); - case "get_layers": - return GetLayers(); // Helper to list current layers - // --- Settings (Example) --- // case "set_resolution": // int? width = @params["width"]?.ToObject(); @@ -144,167 +121,12 @@ public static object HandleCommand(JObject @params) default: return Response.Error( - $"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers." + $"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool." ); } } - // --- Editor State/Info Methods --- - private static object GetEditorState() - { - try - { - var state = new - { - isPlaying = EditorApplication.isPlaying, - isPaused = EditorApplication.isPaused, - isCompiling = EditorApplication.isCompiling, - isUpdating = EditorApplication.isUpdating, - applicationPath = EditorApplication.applicationPath, - applicationContentsPath = EditorApplication.applicationContentsPath, - timeSinceStartup = EditorApplication.timeSinceStartup, - }; - return Response.Success("Retrieved editor state.", state); - } - catch (Exception e) - { - return Response.Error($"Error getting editor state: {e.Message}"); - } - } - - private static object GetProjectRoot() - { - try - { - // Application.dataPath points to /Assets - string assetsPath = Application.dataPath.Replace('\\', '/'); - string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); - if (string.IsNullOrEmpty(projectRoot)) - { - return Response.Error("Could not determine project root from Application.dataPath"); - } - return Response.Success("Project root resolved.", new { projectRoot }); - } - catch (Exception e) - { - return Response.Error($"Error getting project root: {e.Message}"); - } - } - - private static object GetEditorWindows() - { - try - { - // Get all types deriving from EditorWindow - var windowTypes = AppDomain - .CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => type.IsSubclassOf(typeof(EditorWindow))) - .ToList(); - - var openWindows = new List(); - - // Find currently open instances - // Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows - EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll(); - - foreach (EditorWindow window in allWindows) - { - if (window == null) - continue; // Skip potentially destroyed windows - - try - { - openWindows.Add( - new - { - title = window.titleContent.text, - typeName = window.GetType().FullName, - isFocused = EditorWindow.focusedWindow == window, - position = new - { - x = window.position.x, - y = window.position.y, - width = window.position.width, - height = window.position.height, - }, - instanceID = window.GetInstanceID(), - } - ); - } - catch (Exception ex) - { - Debug.LogWarning( - $"Could not get info for window {window.GetType().Name}: {ex.Message}" - ); - } - } - - return Response.Success("Retrieved list of open editor windows.", openWindows); - } - catch (Exception e) - { - return Response.Error($"Error getting editor windows: {e.Message}"); - } - } - - private static object GetPrefabStageInfo() - { - try - { - PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); - if (stage == null) - { - return Response.Success - ("No prefab stage is currently open.", new { isOpen = false }); - } - - return Response.Success( - "Prefab stage info retrieved.", - new - { - isOpen = true, - assetPath = stage.assetPath, - prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null, - mode = stage.mode.ToString(), - isDirty = stage.scene.isDirty - } - ); - } - catch (Exception e) - { - return Response.Error($"Error getting prefab stage info: {e.Message}"); - } - } - - private static object GetActiveTool() - { - try - { - Tool currentTool = UnityEditor.Tools.current; - string toolName = currentTool.ToString(); // Enum to string - bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active - string activeToolName = customToolActive - ? EditorTools.GetActiveToolName() - : toolName; // Get custom name if needed - - var toolInfo = new - { - activeTool = activeToolName, - isCustom = customToolActive, - pivotMode = UnityEditor.Tools.pivotMode.ToString(), - pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), - handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity - handlePosition = UnityEditor.Tools.handlePosition, - }; - - return Response.Success("Retrieved active tool information.", toolInfo); - } - catch (Exception e) - { - return Response.Error($"Error getting active tool: {e.Message}"); - } - } + // --- Tool Control Methods --- private static object SetActiveTool(string toolName) { @@ -341,43 +163,6 @@ private static object SetActiveTool(string toolName) } } - private static object GetSelection() - { - try - { - var selectionInfo = new - { - activeObject = Selection.activeObject?.name, - activeGameObject = Selection.activeGameObject?.name, - activeTransform = Selection.activeTransform?.name, - activeInstanceID = Selection.activeInstanceID, - count = Selection.count, - objects = Selection - .objects.Select(obj => new - { - name = obj?.name, - type = obj?.GetType().FullName, - instanceID = obj?.GetInstanceID(), - }) - .ToList(), - gameObjects = Selection - .gameObjects.Select(go => new - { - name = go?.name, - instanceID = go?.GetInstanceID(), - }) - .ToList(), - assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view - }; - - return Response.Success("Retrieved current selection details.", selectionInfo); - } - catch (Exception e) - { - return Response.Error($"Error getting selection: {e.Message}"); - } - } - // --- Tag Management Methods --- private static object AddTag(string tagName) @@ -386,7 +171,7 @@ private static object AddTag(string tagName) return Response.Error("Tag name cannot be empty or whitespace."); // Check if tag already exists - if (InternalEditorUtility.tags.Contains(tagName)) + if (System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagName)) { return Response.Error($"Tag '{tagName}' already exists."); } @@ -413,7 +198,7 @@ private static object RemoveTag(string tagName) return Response.Error("Cannot remove the built-in 'Untagged' tag."); // Check if tag exists before attempting removal - if (!InternalEditorUtility.tags.Contains(tagName)) + if (!System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagName)) { return Response.Error($"Tag '{tagName}' does not exist."); } @@ -433,19 +218,6 @@ private static object RemoveTag(string tagName) } } - private static object GetTags() - { - try - { - string[] tags = InternalEditorUtility.tags; - return Response.Success("Retrieved current tags.", tags); - } - catch (Exception e) - { - return Response.Error($"Failed to retrieve tags: {e.Message}"); - } - } - // --- Layer Management Methods --- private static object AddLayer(string layerName) @@ -569,27 +341,6 @@ private static object RemoveLayer(string layerName) } } - private static object GetLayers() - { - try - { - var layers = new Dictionary(); - for (int i = 0; i < TotalLayerCount; i++) - { - string layerName = LayerMask.LayerToName(i); - if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names - { - layers.Add(i, layerName); - } - } - return Response.Success("Retrieved current named layers.", layers); - } - catch (Exception e) - { - return Response.Error($"Failed to retrieve layers: {e.Message}"); - } - } - // --- Helper Methods --- /// @@ -605,7 +356,7 @@ private static SerializedObject GetTagManager() ); if (tagManagerAssets == null || tagManagerAssets.Length == 0) { - Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings."); + McpLog.Error("[ManageEditor] TagManager.asset not found in ProjectSettings."); return null; } // The first object in the asset file should be the TagManager @@ -613,7 +364,7 @@ private static SerializedObject GetTagManager() } catch (Exception e) { - Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); + McpLog.Error($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); return null; } } @@ -624,22 +375,4 @@ private static object SetGameViewResolution(int width, int height) { ... } private static object SetQualityLevel(JToken qualityLevelToken) { ... } */ } - - // Helper class to get custom tool names (remains the same) - internal static class EditorTools - { - public static string GetActiveToolName() - { - // This is a placeholder. Real implementation depends on how custom tools - // are registered and tracked in the specific Unity project setup. - // It might involve checking static variables, calling methods on specific tool managers, etc. - if (UnityEditor.Tools.current == Tool.Custom) - { - // Example: Check a known custom tool manager - // if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName; - return "Unknown Custom Tool"; - } - return UnityEditor.Tools.current.ToString(); - } - } } diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs index 6eba6fda..74dac6a4 100644 --- a/MCPForUnity/Editor/Tools/RunTests.cs +++ b/MCPForUnity/Editor/Tools/RunTests.cs @@ -20,7 +20,7 @@ public static async Task HandleCommand(JObject @params) string modeStr = @params?["mode"]?.ToString(); if (string.IsNullOrWhiteSpace(modeStr)) { - modeStr = "edit"; + modeStr = "EditMode"; } if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) diff --git a/MCPForUnity/UnityMcpServer~/src/pyproject.toml b/MCPForUnity/UnityMcpServer~/src/pyproject.toml index 709c6e32..776180dd 100644 --- a/MCPForUnity/UnityMcpServer~/src/pyproject.toml +++ b/MCPForUnity/UnityMcpServer~/src/pyproject.toml @@ -35,6 +35,7 @@ py-modules = [ "server", "telemetry", "telemetry_decorator", - "unity_connection" + "unity_connection", + "unity_instance_middleware" ] -packages = ["tools", "resources", "registry"] +packages = ["tools", "resources", "registry"] \ No newline at end of file diff --git a/MCPForUnity/UnityMcpServer~/src/resources/active_tool.py b/MCPForUnity/UnityMcpServer~/src/resources/active_tool.py new file mode 100644 index 00000000..ed267f72 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/active_tool.py @@ -0,0 +1,46 @@ +from pydantic import BaseModel +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class Vector3(BaseModel): + """3D vector.""" + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + + +class ActiveToolData(BaseModel): + """Active tool data fields.""" + activeTool: str = "" + isCustom: bool = False + pivotMode: str = "" + pivotRotation: str = "" + handleRotation: Vector3 = Vector3() + handlePosition: Vector3 = Vector3() + + +class ActiveToolResponse(MCPResponse): + """Information about the currently active editor tool.""" + data: ActiveToolData = ActiveToolData() + + +@mcp_for_unity_resource( + uri="unity://editor/active-tool", + name="editor_active_tool", + description="Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings." +) +async def get_active_tool(ctx: Context) -> ActiveToolResponse | MCPResponse: + """Get active editor tool information.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_active_tool", + {} + ) + return ActiveToolResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/editor_state.py b/MCPForUnity/UnityMcpServer~/src/resources/editor_state.py new file mode 100644 index 00000000..b4e26689 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/editor_state.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class EditorStateData(BaseModel): + """Editor state data fields.""" + isPlaying: bool = False + isPaused: bool = False + isCompiling: bool = False + isUpdating: bool = False + timeSinceStartup: float = 0.0 + activeSceneName: str = "" + selectionCount: int = 0 + activeObjectName: str | None = None + + +class EditorStateResponse(MCPResponse): + """Dynamic editor state information that changes frequently.""" + data: EditorStateData = EditorStateData() + + +@mcp_for_unity_resource( + uri="unity://editor/state", + name="editor_state", + description="Current editor runtime state including play mode, compilation status, active scene, and selection summary. Refresh frequently for up-to-date information." +) +async def get_editor_state(ctx: Context) -> EditorStateResponse | MCPResponse: + """Get current editor runtime state.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_editor_state", + {} + ) + return EditorStateResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/layers.py b/MCPForUnity/UnityMcpServer~/src/resources/layers.py new file mode 100644 index 00000000..c9f754a5 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/layers.py @@ -0,0 +1,28 @@ +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class LayersResponse(MCPResponse): + """Dictionary of layer indices to layer names.""" + data: dict[int, str] = {} + + +@mcp_for_unity_resource( + uri="unity://project/layers", + name="project_layers", + description="All layers defined in the project's TagManager with their indices (0-31). Read this before using add_layer or remove_layer tools." +) +async def get_layers(ctx: Context) -> LayersResponse | MCPResponse: + """Get all project layers with their indices.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_layers", + {} + ) + return LayersResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py b/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py index 07d5681d..4cf15208 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/menu_items.py @@ -12,10 +12,10 @@ class GetMenuItemsResponse(MCPResponse): @mcp_for_unity_resource( uri="mcpforunity://menu-items", - name="get_menu_items", + name="menu_items", description="Provides a list of all menu items." ) -async def get_menu_items(ctx: Context) -> GetMenuItemsResponse: +async def get_menu_items(ctx: Context) -> GetMenuItemsResponse | MCPResponse: """Provides a list of all menu items. """ unity_instance = get_unity_instance_from_context(ctx) diff --git a/MCPForUnity/UnityMcpServer~/src/resources/prefab_stage.py b/MCPForUnity/UnityMcpServer~/src/resources/prefab_stage.py new file mode 100644 index 00000000..14ef693a --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/prefab_stage.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class PrefabStageData(BaseModel): + """Prefab stage data fields.""" + isOpen: bool = False + assetPath: str | None = None + prefabRootName: str | None = None + mode: str | None = None + isDirty: bool = False + + +class PrefabStageResponse(MCPResponse): + """Information about the current prefab editing context.""" + data: PrefabStageData = PrefabStageData() + + +@mcp_for_unity_resource( + uri="unity://editor/prefab-stage", + name="editor_prefab_stage", + description="Current prefab editing context if a prefab is open in isolation mode. Returns isOpen=false if no prefab is being edited." +) +async def get_prefab_stage(ctx: Context) -> PrefabStageResponse | MCPResponse: + """Get current prefab stage information.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_prefab_stage", + {} + ) + return PrefabStageResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/project_info.py b/MCPForUnity/UnityMcpServer~/src/resources/project_info.py new file mode 100644 index 00000000..ea7691f8 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/project_info.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class ProjectInfoData(BaseModel): + """Project info data fields.""" + projectRoot: str = "" + projectName: str = "" + unityVersion: str = "" + platform: str = "" + assetsPath: str = "" + + +class ProjectInfoResponse(MCPResponse): + """Static project configuration information.""" + data: ProjectInfoData = ProjectInfoData() + + +@mcp_for_unity_resource( + uri="unity://project/info", + name="project_info", + description="Static project information including root path, Unity version, and platform. This data rarely changes." +) +async def get_project_info(ctx: Context) -> ProjectInfoResponse | MCPResponse: + """Get static project configuration information.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_project_info", + {} + ) + return ProjectInfoResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/selection.py b/MCPForUnity/UnityMcpServer~/src/resources/selection.py new file mode 100644 index 00000000..76567cb4 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/selection.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class SelectionObjectInfo(BaseModel): + """Information about a selected object.""" + name: str | None = None + type: str | None = None + instanceID: int | None = None + + +class SelectionGameObjectInfo(BaseModel): + """Information about a selected GameObject.""" + name: str | None = None + instanceID: int | None = None + + +class SelectionData(BaseModel): + """Selection data fields.""" + activeObject: str | None = None + activeGameObject: str | None = None + activeTransform: str | None = None + activeInstanceID: int = 0 + count: int = 0 + objects: list[SelectionObjectInfo] = [] + gameObjects: list[SelectionGameObjectInfo] = [] + assetGUIDs: list[str] = [] + + +class SelectionResponse(MCPResponse): + """Detailed information about the current editor selection.""" + data: SelectionData = SelectionData() + + +@mcp_for_unity_resource( + uri="unity://editor/selection", + name="editor_selection", + description="Detailed information about currently selected objects in the editor, including GameObjects, assets, and their properties." +) +async def get_selection(ctx: Context) -> SelectionResponse | MCPResponse: + """Get detailed editor selection information.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_selection", + {} + ) + return SelectionResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tags.py b/MCPForUnity/UnityMcpServer~/src/resources/tags.py new file mode 100644 index 00000000..d4fec612 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/tags.py @@ -0,0 +1,29 @@ +from pydantic import Field +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class TagsResponse(MCPResponse): + """List of all tags in the project.""" + data: list[str] = Field(default_factory=list) + + +@mcp_for_unity_resource( + uri="unity://project/tags", + name="project_tags", + description="All tags defined in the project's TagManager. Read this before using add_tag or remove_tag tools." +) +async def get_tags(ctx: Context) -> TagsResponse | MCPResponse: + """Get all project tags.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_tags", + {} + ) + return TagsResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/resources/tests.py b/MCPForUnity/UnityMcpServer~/src/resources/tests.py index 7fcc056a..a229466b 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/tests.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/tests.py @@ -21,7 +21,7 @@ class GetTestsResponse(MCPResponse): @mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.") -async def get_tests(ctx: Context) -> GetTestsResponse: +async def get_tests(ctx: Context) -> GetTestsResponse | MCPResponse: """Provides a list of all tests. """ unity_instance = get_unity_instance_from_context(ctx) @@ -38,7 +38,7 @@ async def get_tests(ctx: Context) -> GetTestsResponse: async def get_tests_for_mode( ctx: Context, mode: Annotated[Literal["EditMode", "PlayMode"], Field(description="The mode to filter tests by.")], -) -> GetTestsResponse: +) -> GetTestsResponse | MCPResponse: """Provides a list of tests for a specific mode. Args: diff --git a/MCPForUnity/UnityMcpServer~/src/resources/unity_instances.py b/MCPForUnity/UnityMcpServer~/src/resources/unity_instances.py index 0d2df784..c716ea35 100644 --- a/MCPForUnity/UnityMcpServer~/src/resources/unity_instances.py +++ b/MCPForUnity/UnityMcpServer~/src/resources/unity_instances.py @@ -13,7 +13,7 @@ name="unity_instances", description="Lists all running Unity Editor instances with their details." ) -def unity_instances(ctx: Context) -> dict[str, Any]: +async def unity_instances(ctx: Context) -> dict[str, Any]: """ List all available Unity Editor instances. @@ -30,7 +30,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]: Returns: Dictionary containing list of instances and metadata """ - ctx.info("Listing Unity instances") + await ctx.info("Listing Unity instances") try: pool = get_unity_connection_pool() @@ -58,7 +58,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]: return result except Exception as e: - ctx.error(f"Error listing Unity instances: {e}") + await ctx.error(f"Error listing Unity instances: {e}") return { "success": False, "error": f"Failed to list Unity instances: {str(e)}", diff --git a/MCPForUnity/UnityMcpServer~/src/resources/windows.py b/MCPForUnity/UnityMcpServer~/src/resources/windows.py new file mode 100644 index 00000000..c52d58c9 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/resources/windows.py @@ -0,0 +1,46 @@ +from pydantic import BaseModel +from fastmcp import Context + +from models import MCPResponse +from registry import mcp_for_unity_resource +from tools import get_unity_instance_from_context, async_send_with_unity_instance +from unity_connection import async_send_command_with_retry + + +class WindowPosition(BaseModel): + """Window position and size.""" + x: float = 0.0 + y: float = 0.0 + width: float = 0.0 + height: float = 0.0 + + +class WindowInfo(BaseModel): + """Information about an editor window.""" + title: str = "" + typeName: str = "" + isFocused: bool = False + position: WindowPosition = WindowPosition() + instanceID: int = 0 + + +class WindowsResponse(MCPResponse): + """List of all open editor windows.""" + data: list[WindowInfo] = [] + + +@mcp_for_unity_resource( + uri="unity://editor/windows", + name="editor_windows", + description="All currently open editor windows with their titles, types, positions, and focus state." +) +async def get_windows(ctx: Context) -> WindowsResponse | MCPResponse: + """Get all open editor windows.""" + unity_instance = get_unity_instance_from_context(ctx) + response = await async_send_with_unity_instance( + async_send_command_with_retry, + unity_instance, + "get_windows", + {} + ) + return WindowsResponse(**response) if isinstance(response, dict) else response diff --git a/MCPForUnity/UnityMcpServer~/src/server.py b/MCPForUnity/UnityMcpServer~/src/server.py index 48c33ff4..c7d06c32 100644 --- a/MCPForUnity/UnityMcpServer~/src/server.py +++ b/MCPForUnity/UnityMcpServer~/src/server.py @@ -172,23 +172,36 @@ def _emit_startup(): name="mcp-for-unity-server", lifespan=server_lifespan, instructions=""" -This server provides tools to interact with the Unity Game Engine Editor.\n\n -Available tools:\n -- `manage_editor`: Controls editor state and queries info.\n -- `execute_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n -- `read_console`: Reads or clears Unity console messages, with filtering options.\n -- `manage_scene`: Manages scenes.\n -- `manage_gameobject`: Manages GameObjects in the scene.\n -- `manage_script`: Manages C# script files.\n -- `manage_asset`: Manages prefabs and assets.\n -- `manage_shader`: Manages shaders.\n\n -- Tips:\n -- Create prefabs for reusable GameObjects.\n -- Always include a camera and main light in your scenes.\n -- Unless specified otherwise, paths are relative to the project's `Assets/` folder.\n -- After creating or modifying scripts with `manage_script`, allow Unity to recompile; use `read_console` to check for compile errors.\n -- Use `execute_menu_item` for interacting with Unity systems and third party tools like a user would.\n +This server provides tools to interact with the Unity Game Engine Editor. +Important Workflows: + +Resources vs Tools: +- Use RESOURCES to read editor state (editor_state, project_info, project_tags, tests, etc) +- Use TOOLS to perform actions and mutations (manage_editor for play mode control, tag/layer management, etc) +- Always check related resources before modifying the engine state with tools + +Script Management: +- After creating or modifying scripts (by your own tools or the `manage_script` tool) use `read_console` to check for compilation errors before proceeding +- Only after successful compilation can new components/types be used +- You can poll the `editor_state` resource's `isCompiling` field to check if the domain reload is complete + +Scene Setup: +- Always include a Camera and main Light (Directional Light) in new scenes +- Create prefabs with `manage_asset` for reusable GameObjects +- Use `manage_scene` to load, save, and query scene information + +Path Conventions: +- Unless specified otherwise, all paths are relative to the project's `Assets/` folder +- Use forward slashes (/) in paths for cross-platform compatibility + +Console Monitoring: +- Check `read_console` regularly to catch errors, warnings, and compilation status +- Filter by log type (Error, Warning, Log) to focus on specific issues + +Menu Items: +- Use `execute_menu_item` when you have read the menu items resource +- This lets you interact with Unity's menu system and third-party tools """ ) diff --git a/MCPForUnity/UnityMcpServer~/src/tests/__init__.py b/MCPForUnity/UnityMcpServer~/src/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/__init__.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/__init__.py similarity index 100% rename from tests/__init__.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/__init__.py diff --git a/MCPForUnity/UnityMcpServer~/src/tests/integration/conftest.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/conftest.py new file mode 100644 index 00000000..d8f3a7e6 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/conftest.py @@ -0,0 +1,66 @@ +import os +import sys +import types + +# Ensure telemetry is disabled during test collection and execution to avoid +# any background network or thread startup that could slow or block pytest. +os.environ.setdefault("DISABLE_TELEMETRY", "true") +os.environ.setdefault("UNITY_MCP_DISABLE_TELEMETRY", "true") +os.environ.setdefault("MCP_DISABLE_TELEMETRY", "true") + +# NOTE: These tests are integration tests for the MCP server Python code. +# They test tools, resources, and utilities without requiring Unity to be running. +# Tests can now import directly from the parent package since they're inside src/ +# To run: cd MCPForUnity/UnityMcpServer~/src && uv run pytest tests/integration/ -v + +# Stub telemetry modules to avoid file I/O during import of tools package +telemetry = types.ModuleType("telemetry") +def _noop(*args, **kwargs): + pass +class MilestoneType: + pass +telemetry.record_resource_usage = _noop +telemetry.record_tool_usage = _noop +telemetry.record_milestone = _noop +telemetry.MilestoneType = MilestoneType +telemetry.get_package_version = lambda: "0.0.0" +sys.modules.setdefault("telemetry", telemetry) + +telemetry_decorator = types.ModuleType("telemetry_decorator") +def telemetry_tool(*dargs, **dkwargs): + def _wrap(fn): + return fn + return _wrap +telemetry_decorator.telemetry_tool = telemetry_tool +sys.modules.setdefault("telemetry_decorator", telemetry_decorator) + +# Stub fastmcp module (not mcp.server.fastmcp) +fastmcp = types.ModuleType("fastmcp") + +class _DummyFastMCP: + pass + +class _DummyContext: + pass + +class _DummyMiddleware: + """Base middleware class stub.""" + pass + +class _DummyMiddlewareContext: + """Middleware context stub.""" + pass + +fastmcp.FastMCP = _DummyFastMCP +fastmcp.Context = _DummyContext +sys.modules.setdefault("fastmcp", fastmcp) + +# Stub fastmcp.server.middleware submodule +fastmcp_server = types.ModuleType("fastmcp.server") +fastmcp_server_middleware = types.ModuleType("fastmcp.server.middleware") +fastmcp_server_middleware.Middleware = _DummyMiddleware +fastmcp_server_middleware.MiddlewareContext = _DummyMiddlewareContext +fastmcp.server = fastmcp_server +fastmcp_server.middleware = fastmcp_server_middleware +sys.modules.setdefault("fastmcp.server", fastmcp_server) +sys.modules.setdefault("fastmcp.server.middleware", fastmcp_server_middleware) diff --git a/tests/test_edit_normalization_and_noop.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_edit_normalization_and_noop.py similarity index 89% rename from tests/test_edit_normalization_and_noop.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_edit_normalization_and_noop.py index c2232fc4..377a4c86 100644 --- a/tests/test_edit_normalization_and_noop.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_edit_normalization_and_noop.py @@ -1,23 +1,4 @@ -import sys -import pathlib -import importlib.util - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def _load(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -manage_script = _load(SRC / "tools" / "manage_script.py", "manage_script_mod2") -manage_script_edits = _load( - SRC / "tools" / "script_apply_edits.py", "script_apply_edits_mod2") +from .test_helpers import DummyContext class DummyMCP: @@ -28,9 +9,6 @@ def deco(fn): self.tools[fn.__name__] = fn; return fn return deco -from tests.test_helpers import DummyContext - - def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration diff --git a/tests/test_edit_strict_and_warnings.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_edit_strict_and_warnings.py similarity index 62% rename from tests/test_edit_strict_and_warnings.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_edit_strict_and_warnings.py index ba5ed06b..2914d7db 100644 --- a/tests/test_edit_strict_and_warnings.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_edit_strict_and_warnings.py @@ -1,53 +1,4 @@ -import sys -import pathlib -import importlib.util -import types - -from tests.test_helpers import DummyContext - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - -# Stub telemetry modules to avoid file I/O during import of tools package -telemetry = types.ModuleType("telemetry") -def _noop(*args, **kwargs): - pass -class MilestoneType: - pass -telemetry.record_resource_usage = _noop -telemetry.record_tool_usage = _noop -telemetry.record_milestone = _noop -telemetry.MilestoneType = MilestoneType -telemetry.get_package_version = lambda: "0.0.0" -sys.modules.setdefault("telemetry", telemetry) - -telemetry_decorator = types.ModuleType("telemetry_decorator") -def telemetry_tool(*dargs, **dkwargs): - def _wrap(fn): - return fn - return _wrap -telemetry_decorator.telemetry_tool = telemetry_tool -sys.modules.setdefault("telemetry_decorator", telemetry_decorator) - -# stub mcp.server.fastmcp -mcp_pkg = types.ModuleType("mcp") -server_pkg = types.ModuleType("mcp.server") -fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") - - -class _Dummy: - pass - - -fastmcp_pkg.FastMCP = _Dummy -fastmcp_pkg.Context = _Dummy -server_pkg.fastmcp = fastmcp_pkg -mcp_pkg.server = server_pkg -sys.modules.setdefault("mcp", mcp_pkg) -sys.modules.setdefault("mcp.server", server_pkg) -sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) +from .test_helpers import DummyContext class DummyMCP: diff --git a/tests/test_find_in_file_minimal.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_find_in_file_minimal.py similarity index 61% rename from tests/test_find_in_file_minimal.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_find_in_file_minimal.py index 0d49dc09..399deef5 100644 --- a/tests/test_find_in_file_minimal.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_find_in_file_minimal.py @@ -1,36 +1,7 @@ -import sys -import pathlib -import importlib.util -import types import asyncio import pytest -from tests.test_helpers import DummyContext - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - -# Stub telemetry modules to avoid file I/O during import of tools package -telemetry = types.ModuleType("telemetry") -def _noop(*args, **kwargs): - pass -class MilestoneType: - pass -telemetry.record_resource_usage = _noop -telemetry.record_tool_usage = _noop -telemetry.record_milestone = _noop -telemetry.MilestoneType = MilestoneType -telemetry.get_package_version = lambda: "0.0.0" -sys.modules.setdefault("telemetry", telemetry) - -telemetry_decorator = types.ModuleType("telemetry_decorator") -def telemetry_tool(*dargs, **dkwargs): - def _wrap(fn): - return fn - return _wrap -telemetry_decorator.telemetry_tool = telemetry_tool -sys.modules.setdefault("telemetry_decorator", telemetry_decorator) +from .test_helpers import DummyContext class DummyMCP: diff --git a/tests/test_get_sha.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_get_sha.py similarity index 75% rename from tests/test_get_sha.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_get_sha.py index 3e9a2261..bfd110d5 100644 --- a/tests/test_get_sha.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_get_sha.py @@ -1,24 +1,4 @@ -import sys -import pathlib -import importlib.util - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def _load_module(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Cannot load module {name} from {path}") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -manage_script = _load_module( - SRC / "tools" / "manage_script.py", "manage_script_mod") +from .test_helpers import DummyContext class DummyMCP: @@ -32,9 +12,6 @@ def deco(fn): return deco -from tests.test_helpers import DummyContext - - def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration diff --git a/tests/test_helpers.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_helpers.py similarity index 100% rename from tests/test_helpers.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_helpers.py diff --git a/tests/test_improved_anchor_matching.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_improved_anchor_matching.py similarity index 87% rename from tests/test_improved_anchor_matching.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_improved_anchor_matching.py index e56b8728..5f06cd17 100644 --- a/tests/test_improved_anchor_matching.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_improved_anchor_matching.py @@ -2,25 +2,9 @@ Test the improved anchor matching logic. """ -import sys -import pathlib -import importlib.util +import re -# add server src to path and load modules -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def load_module(path, name): - spec = importlib.util.spec_from_file_location(name, path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -script_apply_edits_module = load_module( - SRC / "tools" / "script_apply_edits.py", "script_apply_edits_module") +import tools.script_apply_edits as script_apply_edits_module def test_improved_anchor_matching(): @@ -41,8 +25,6 @@ def test_improved_anchor_matching(): } }''' - import re - # Test the problematic anchor pattern anchor_pattern = r"\s*}\s*$" flags = re.MULTILINE diff --git a/tests/test_instance_routing_comprehensive.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_instance_routing_comprehensive.py similarity index 98% rename from tests/test_instance_routing_comprehensive.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_instance_routing_comprehensive.py index ccb1da68..089770b6 100644 --- a/tests/test_instance_routing_comprehensive.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_instance_routing_comprehensive.py @@ -10,18 +10,10 @@ - get_unity_instance_from_context() reads from ctx.get_state() - All tools (GameObject, Script, Asset, etc.) use get_unity_instance_from_context() """ -import sys -import pathlib import pytest from unittest.mock import AsyncMock, Mock, MagicMock, patch from fastmcp import Context -# Add Server source to path -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "Server" -if str(SRC) not in sys.path: - sys.path.insert(0, str(SRC)) - from unity_instance_middleware import UnityInstanceMiddleware from tools import get_unity_instance_from_context diff --git a/tests/test_instance_targeting_resolution.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_instance_targeting_resolution.py similarity index 91% rename from tests/test_instance_targeting_resolution.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_instance_targeting_resolution.py index e0cae2fb..0a961ccf 100644 --- a/tests/test_instance_targeting_resolution.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_instance_targeting_resolution.py @@ -1,11 +1,4 @@ -import sys -import pathlib -from tests.test_helpers import DummyContext - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -if str(SRC) not in sys.path: - sys.path.insert(0, str(SRC)) +from .test_helpers import DummyContext def test_manage_gameobject_uses_session_state(monkeypatch): diff --git a/tests/test_json_parsing_simple.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_json_parsing_simple.py similarity index 100% rename from tests/test_json_parsing_simple.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_json_parsing_simple.py diff --git a/tests/test_logging_stdout.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_logging_stdout.py similarity index 100% rename from tests/test_logging_stdout.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_logging_stdout.py diff --git a/tests/test_manage_asset_json_parsing.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_asset_json_parsing.py similarity index 99% rename from tests/test_manage_asset_json_parsing.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_asset_json_parsing.py index 466c7b0d..44a944fa 100644 --- a/tests/test_manage_asset_json_parsing.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_asset_json_parsing.py @@ -4,7 +4,7 @@ import pytest import json -from tests.test_helpers import DummyContext +from .test_helpers import DummyContext from tools.manage_asset import manage_asset diff --git a/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_asset_param_coercion.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_asset_param_coercion.py new file mode 100644 index 00000000..f3db0314 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_asset_param_coercion.py @@ -0,0 +1,34 @@ +import asyncio + +from .test_helpers import DummyContext +import tools.manage_asset as manage_asset_mod + + +def test_manage_asset_pagination_coercion(monkeypatch): + captured = {} + + async def fake_async_send(cmd, params, **kwargs): + captured["params"] = params + return {"success": True, "data": {}} + + monkeypatch.setattr(manage_asset_mod, "async_send_command_with_retry", fake_async_send) + + result = asyncio.run( + manage_asset_mod.manage_asset( + ctx=DummyContext(), + action="search", + path="Assets", + page_size="50", + page_number="2", + ) + ) + + assert result == {"success": True, "data": {}} + assert captured["params"]["pageSize"] == 50 + assert captured["params"]["pageNumber"] == 2 + + + + + + diff --git a/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_gameobject_param_coercion.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_gameobject_param_coercion.py new file mode 100644 index 00000000..f5c4d044 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_gameobject_param_coercion.py @@ -0,0 +1,31 @@ +from .test_helpers import DummyContext +import tools.manage_gameobject as manage_go_mod + + +def test_manage_gameobject_boolean_and_tag_mapping(monkeypatch): + captured = {} + + def fake_send(cmd, params): + captured["params"] = params + return {"success": True, "data": {}} + + monkeypatch.setattr(manage_go_mod, "send_command_with_retry", fake_send) + + # find by tag: allow tag to map to searchTerm + resp = manage_go_mod.manage_gameobject( + ctx=DummyContext(), + action="find", + search_method="by_tag", + tag="Player", + find_all="true", + search_inactive="0", + ) + # Loosen equality: wrapper may include a diagnostic message + assert resp.get("success") is True + assert "data" in resp + # ensure tag mapped to searchTerm and booleans passed through; C# side coerces true/false already + assert captured["params"]["searchTerm"] == "Player" + assert captured["params"]["findAll"] == "true" or captured["params"]["findAll"] is True + assert captured["params"]["searchInactive"] in ("0", False, 0) + + diff --git a/tests/test_manage_script_uri.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_script_uri.py similarity index 80% rename from tests/test_manage_script_uri.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_script_uri.py index e5565834..7e2f0558 100644 --- a/tests/test_manage_script_uri.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_manage_script_uri.py @@ -1,40 +1,6 @@ -# import triggers registration elsewhere; no direct use here -import sys -import types -from pathlib import Path - import pytest - -# Locate server src dynamically to avoid hardcoded layout assumptions (same as other tests) -ROOT = Path(__file__).resolve().parents[1] -candidates = [ - ROOT / "MCPForUnity" / "UnityMcpServer~" / "src", - ROOT / "UnityMcpServer~" / "src", -] -SRC = next((p for p in candidates if p.exists()), None) -if SRC is None: - searched = "\n".join(str(p) for p in candidates) - pytest.skip( - "MCP for Unity server source not found. Tried:\n" + searched, - allow_module_level=True, - ) -sys.path.insert(0, str(SRC)) - -# Stub fastmcp to avoid real MCP deps -fastmcp_pkg = types.ModuleType("fastmcp") - - -class _Dummy: - pass - - -fastmcp_pkg.FastMCP = _Dummy -fastmcp_pkg.Context = _Dummy -sys.modules.setdefault("fastmcp", fastmcp_pkg) - - -# Import target module after path injection +from .test_helpers import DummyContext class DummyMCP: @@ -48,12 +14,6 @@ def _decorator(fn): return _decorator -# (removed unused DummyCtx) - - -from tests.test_helpers import DummyContext - - def _register_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration diff --git a/tests/test_read_console_truncate.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_read_console_truncate.py similarity index 79% rename from tests/test_read_console_truncate.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_read_console_truncate.py index 850126b1..63143f74 100644 --- a/tests/test_read_console_truncate.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_read_console_truncate.py @@ -1,23 +1,4 @@ -import sys -import pathlib -import importlib.util - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def _load_module(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Cannot load module {name} from {path}") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -read_console_mod = _load_module( - SRC / "tools" / "read_console.py", "read_console_mod") +from .test_helpers import DummyContext class DummyMCP: @@ -31,9 +12,6 @@ def deco(fn): return deco -from tests.test_helpers import DummyContext - - def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration diff --git a/tests/test_read_resource_minimal.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_read_resource_minimal.py similarity index 69% rename from tests/test_read_resource_minimal.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_read_resource_minimal.py index 4d171926..cd3fa24a 100644 --- a/tests/test_read_resource_minimal.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_read_resource_minimal.py @@ -1,30 +1,7 @@ -import sys -import pathlib import asyncio -import types import pytest -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - -# Stub mcp.server.fastmcp to satisfy imports without full package -mcp_pkg = types.ModuleType("mcp") -server_pkg = types.ModuleType("mcp.server") -fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") - - -class _Dummy: - pass - - -fastmcp_pkg.FastMCP = _Dummy -fastmcp_pkg.Context = _Dummy -server_pkg.fastmcp = fastmcp_pkg -mcp_pkg.server = server_pkg -sys.modules.setdefault("mcp", mcp_pkg) -sys.modules.setdefault("mcp.server", server_pkg) -sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) +from .test_helpers import DummyContext class DummyMCP: @@ -38,9 +15,6 @@ def deco(fn): return deco -from tests.test_helpers import DummyContext - - @pytest.fixture() def resource_tools(): mcp = DummyMCP() diff --git a/tests/test_resources_api.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_resources_api.py similarity index 71% rename from tests/test_resources_api.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_resources_api.py index 616df404..d8bca76b 100644 --- a/tests/test_resources_api.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_resources_api.py @@ -1,33 +1,6 @@ -import sys -from pathlib import Path import pytest -import types -# locate server src dynamically to avoid hardcoded layout assumptions -ROOT = Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - -# Stub telemetry modules to avoid file I/O during import of tools package -telemetry = types.ModuleType("telemetry") -def _noop(*args, **kwargs): - pass -class MilestoneType: # minimal placeholder - pass -telemetry.record_resource_usage = _noop -telemetry.record_tool_usage = _noop -telemetry.record_milestone = _noop -telemetry.MilestoneType = MilestoneType -telemetry.get_package_version = lambda: "0.0.0" -sys.modules.setdefault("telemetry", telemetry) - -telemetry_decorator = types.ModuleType("telemetry_decorator") -def telemetry_tool(*_args, **_kwargs): - def _wrap(fn): - return fn - return _wrap -telemetry_decorator.telemetry_tool = telemetry_tool -sys.modules.setdefault("telemetry_decorator", telemetry_decorator) +from .test_helpers import DummyContext class DummyMCP: @@ -41,9 +14,6 @@ def deco(fn): return deco -from tests.test_helpers import DummyContext - - @pytest.fixture() def resource_tools(): mcp = DummyMCP() diff --git a/tests/test_script_editing.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_script_editing.py similarity index 100% rename from tests/test_script_editing.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_script_editing.py diff --git a/tests/test_script_tools.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_script_tools.py similarity index 90% rename from tests/test_script_tools.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_script_tools.py index 43255722..e8755f45 100644 --- a/tests/test_script_tools.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_script_tools.py @@ -1,26 +1,7 @@ -import sys -import pathlib -import importlib.util import pytest import asyncio -# add server src to path and load modules without triggering package imports -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def load_module(path, name): - spec = importlib.util.spec_from_file_location(name, path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -manage_script_module = load_module( - SRC / "tools" / "manage_script.py", "manage_script_module") -manage_asset_module = load_module( - SRC / "tools" / "manage_asset.py", "manage_asset_module") +from .test_helpers import DummyContext class DummyMCP: @@ -34,9 +15,6 @@ def decorator(func): return decorator -from tests.test_helpers import DummyContext - - def setup_manage_script(): mcp = DummyMCP() # Import the tools module to trigger decorator registration diff --git a/tests/test_telemetry_endpoint_validation.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_endpoint_validation.py similarity index 72% rename from tests/test_telemetry_endpoint_validation.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_endpoint_validation.py index cccc0d6b..cbcc98a0 100644 --- a/tests/test_telemetry_endpoint_validation.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_endpoint_validation.py @@ -7,14 +7,7 @@ def test_endpoint_rejects_non_http(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd") - # Import the telemetry module from the correct path - import sys - import pathlib - ROOT = pathlib.Path(__file__).resolve().parents[1] - SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" - sys.path.insert(0, str(SRC)) - - monkeypatch.chdir(str(SRC)) + # Import the telemetry module telemetry = importlib.import_module("telemetry") importlib.reload(telemetry) @@ -29,18 +22,10 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch): monkeypatch.delenv("UNITY_MCP_TELEMETRY_ENDPOINT", raising=False) # Patch config.telemetry_endpoint via import mocking - import importlib - import sys - import pathlib - ROOT = pathlib.Path(__file__).resolve().parents[1] - SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" - sys.path.insert(0, str(SRC)) - cfg_mod = importlib.import_module("config") old_endpoint = cfg_mod.config.telemetry_endpoint cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry" try: - monkeypatch.chdir(str(SRC)) telemetry = importlib.import_module("telemetry") importlib.reload(telemetry) tc = telemetry.TelemetryCollector() @@ -50,7 +35,6 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch): # Env should override config monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "https://override.example/ep") - monkeypatch.chdir(str(SRC)) importlib.reload(telemetry) tc2 = telemetry.TelemetryCollector() assert tc2.config.endpoint == "https://override.example/ep" @@ -61,14 +45,7 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch): def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch): monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) - # Import the telemetry module from the correct path - import sys - import pathlib - ROOT = pathlib.Path(__file__).resolve().parents[1] - SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" - sys.path.insert(0, str(SRC)) - - monkeypatch.chdir(str(SRC)) + # Import the telemetry module telemetry = importlib.import_module("telemetry") importlib.reload(telemetry) diff --git a/tests/test_telemetry_queue_worker.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_queue_worker.py similarity index 52% rename from tests/test_telemetry_queue_worker.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_queue_worker.py index d992440a..70b558bf 100644 --- a/tests/test_telemetry_queue_worker.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_queue_worker.py @@ -1,60 +1,9 @@ -import sys -import pathlib -import importlib.util -import os import types import threading import time import queue as q - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - -# Stub mcp.server.fastmcp to satisfy imports without the full dependency -mcp_pkg = types.ModuleType("mcp") -server_pkg = types.ModuleType("mcp.server") -fastmcp_pkg = types.ModuleType("mcp.server.fastmcp") - - -class _Dummy: - pass - - -fastmcp_pkg.FastMCP = _Dummy -fastmcp_pkg.Context = _Dummy -server_pkg.fastmcp = fastmcp_pkg -mcp_pkg.server = server_pkg -sys.modules.setdefault("mcp", mcp_pkg) -sys.modules.setdefault("mcp.server", server_pkg) -sys.modules.setdefault("mcp.server.fastmcp", fastmcp_pkg) - -# Ensure telemetry module has get_package_version stub before importing -telemetry_stub = types.ModuleType("telemetry") -telemetry_stub.get_package_version = lambda: "0.0.0" -sys.modules.setdefault("telemetry", telemetry_stub) - - -def _load_module(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Cannot load module {name} from {path}") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -# Load real telemetry on top of stub (it will reuse stubbed helpers) -# Note: CWD change required because telemetry.py calls get_package_version() -# at module load time, which reads pyproject.toml using a relative path. -# This is fragile but necessary given current telemetry module design. -_prev_cwd = os.getcwd() -os.chdir(str(SRC)) -try: - telemetry = _load_module(SRC / "telemetry.py", "telemetry_mod") -finally: - os.chdir(_prev_cwd) +import telemetry def test_telemetry_queue_backpressure_and_single_worker(monkeypatch, caplog): diff --git a/tests/test_telemetry_subaction.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_subaction.py similarity index 95% rename from tests/test_telemetry_subaction.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_subaction.py index 38838a04..ca081b22 100644 --- a/tests/test_telemetry_subaction.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_telemetry_subaction.py @@ -6,10 +6,7 @@ def _get_decorator_module(): import sys import pathlib import types - ROOT = pathlib.Path(__file__).resolve().parents[1] - SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" - if str(SRC) not in sys.path: - sys.path.insert(0, str(SRC)) + # Tests can now import directly from parent package # Remove any previously stubbed module to force real import sys.modules.pop("telemetry_decorator", None) # Preload a minimal telemetry stub to satisfy telemetry_decorator imports diff --git a/tests/test_transport_framing.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_transport_framing.py similarity index 99% rename from tests/test_transport_framing.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_transport_framing.py index a9a3158e..b35c645b 100644 --- a/tests/test_transport_framing.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_transport_framing.py @@ -23,7 +23,7 @@ "MCP for Unity server source not found. Tried:\n" + searched, allow_module_level=True, ) -sys.path.insert(0, str(SRC)) +# Tests can now import directly from parent package def start_dummy_server(greeting: bytes, respond_ping: bool = False): diff --git a/tests/test_validate_script_summary.py b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_validate_script_summary.py similarity index 73% rename from tests/test_validate_script_summary.py rename to MCPForUnity/UnityMcpServer~/src/tests/integration/test_validate_script_summary.py index 23ccad6d..9f347f61 100644 --- a/tests/test_validate_script_summary.py +++ b/MCPForUnity/UnityMcpServer~/src/tests/integration/test_validate_script_summary.py @@ -1,23 +1,4 @@ -import sys -import pathlib -import importlib.util - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def _load_module(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Cannot load module {name} from {path}") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -manage_script = _load_module( - SRC / "tools" / "manage_script.py", "manage_script_mod") +from .test_helpers import DummyContext class DummyMCP: @@ -31,9 +12,6 @@ def deco(fn): return deco -from tests.test_helpers import DummyContext - - def setup_tools(): mcp = DummyMCP() # Import the tools module to trigger decorator registration diff --git a/MCPForUnity/UnityMcpServer~/src/tests/pytest.ini b/MCPForUnity/UnityMcpServer~/src/tests/pytest.ini new file mode 100644 index 00000000..42909991 --- /dev/null +++ b/MCPForUnity/UnityMcpServer~/src/tests/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +markers = + integration: Integration tests that test multiple components together + unit: Unit tests for individual functions or classes diff --git a/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py b/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py index 35234a53..7d5a89a3 100644 --- a/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py +++ b/MCPForUnity/UnityMcpServer~/src/tools/run_tests.py @@ -44,11 +44,11 @@ class RunTestsResponse(MCPResponse): ) async def run_tests( ctx: Context, - mode: Annotated[Literal["edit", "play"], "Unity test mode to run"] = "edit", - timeout_seconds: Annotated[int | str | None, "Optional timeout in seconds for the Unity test run (string, e.g. '30')"] = None, -) -> dict[str, Any]: - # Get active instance from session state - # Removed session_state import + mode: Annotated[Literal["EditMode", "PlayMode"], Field( + description="Unity test mode to run")] = "EditMode", + timeout_seconds: Annotated[int | str, Field( + description="Optional timeout in seconds for the Unity test run (string, e.g. '30')")] | None = None, +) -> RunTestsResponse: unity_instance = get_unity_instance_from_context(ctx) # Coerce timeout defensively (string/float -> int) diff --git a/MCPForUnity/UnityMcpServer~/src/uv.lock b/MCPForUnity/UnityMcpServer~/src/uv.lock index ba563001..92290897 100644 --- a/MCPForUnity/UnityMcpServer~/src/uv.lock +++ b/MCPForUnity/UnityMcpServer~/src/uv.lock @@ -56,6 +56,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/09/9003e5662691056e0e8b2e6f57c799e71875fac0be0e785d8cb11557cd2a/beartype-0.22.5.tar.gz", hash = "sha256:516a9096cc77103c96153474fa35c3ebcd9d36bd2ec8d0e3a43307ced0fa6341", size = 1586256, upload-time = "2025-11-01T05:49:20.771Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/f6/073d19f7b571c08327fbba3f8e011578da67ab62a11f98911274ff80653f/beartype-0.22.5-py3-none-any.whl", hash = "sha256:d9743dd7cd6d193696eaa1e025f8a70fb09761c154675679ff236e61952dfba0", size = 1321700, upload-time = "2025-11-01T05:49:18.436Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -339,6 +366,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/0e/0a22e076944600aeb06f40b7e03bbd762a42d56d43a2f5f4ab954aed9005/cyclopts-4.0.0-py3-none-any.whl", hash = "sha256:e64801a2c86b681f08323fd50110444ee961236a0bae402a66d2cc3feda33da7", size = 178837, upload-time = "2025-10-20T18:33:00.191Z" }, ] +[[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" @@ -393,24 +429,27 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.12.5" +version = "2.13.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonschema-path" }, { name = "mcp" }, - { name = "openapi-core" }, { name = "openapi-pydantic" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/a6/e3b46cd3e228635e0064c2648788b6f66a53bf0d0ddbf5fb44cca951f908/fastmcp-2.12.5.tar.gz", hash = "sha256:2dfd02e255705a4afe43d26caddbc864563036e233dbc6870f389ee523b39a6a", size = 7190263, upload-time = "2025-10-17T13:24:58.896Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/74/584a152bcd174c99ddf3cfdd7e86ec4a6c696fb190a907c2a2ec9056bda2/fastmcp-2.13.0.2.tar.gz", hash = "sha256:d35386561b6f3cde195ba2b5892dc89b8919a721e6b39b98e7a16f9a7c0b8e8b", size = 7762083, upload-time = "2025-10-28T13:56:21.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/c1/9fb98c9649e15ea8cc691b4b09558b61dafb3dc0345f7322f8c4a8991ade/fastmcp-2.12.5-py3-none-any.whl", hash = "sha256:b1e542f9b83dbae7cecfdc9c73b062f77074785abda9f2306799116121344133", size = 329099, upload-time = "2025-10-17T13:24:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c6/95eacd687cfab64fec13bfb64e6c6e7da13d01ecd4cb7d7e991858a08119/fastmcp-2.13.0.2-py3-none-any.whl", hash = "sha256:eb381eb073a101aabbc0ac44b05e23fef0cd1619344b7703115c825c8755fa1c", size = 367511, upload-time = "2025-10-28T13:56:18.83Z" }, ] [[package]] @@ -468,6 +507,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -478,12 +529,48 @@ wheels = [ ] [[package]] -name = "isodate" -version = "0.7.2" +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, + { 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]] @@ -529,48 +616,21 @@ wheels = [ ] [[package]] -name = "lazy-object-proxy" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, - { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, - { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, - { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, - { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, - { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, - { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, - { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, - { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, - { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, - { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, ] [[package]] @@ -585,94 +645,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - [[package]] name = "mcp" -version = "1.16.0" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -681,15 +656,16 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/22/fae38092e6c2995c03232635028510d77e7decff31b4ae79dfa0ba99c635/mcp-1.20.0.tar.gz", hash = "sha256:9ccc09eaadbfbcbbdab1c9723cfe2e0d1d9e324d7d3ce7e332ef90b09ed35177", size = 451377, upload-time = "2025-10-30T22:14:53.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" }, + { url = "https://files.pythonhosted.org/packages/df/00/76fc92f4892d47fecb37131d0e95ea69259f077d84c68f6793a0d96cfe80/mcp-1.20.0-py3-none-any.whl", hash = "sha256:d0dc06f93653f7432ff89f694721c87f79876b6f93741bf628ad1e48f7ac5e5d", size = 173136, upload-time = "2025-10-30T22:14:51.078Z" }, ] [[package]] @@ -712,7 +688,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "fastmcp", specifier = ">=2.12.5" }, + { name = "fastmcp", specifier = ">=2.13.0" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "mcp", specifier = ">=1.16.0" }, { name = "pydantic", specifier = ">=2.12.0" }, @@ -740,26 +716,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] -[[package]] -name = "openapi-core" -version = "0.19.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "isodate" }, - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "more-itertools" }, - { name = "openapi-schema-validator" }, - { name = "openapi-spec-validator" }, - { name = "parse" }, - { name = "typing-extensions" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, -] - [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -773,68 +729,86 @@ wheels = [ ] [[package]] -name = "openapi-schema-validator" -version = "0.6.3" +name = "packaging" +version = "25.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "rfc3339-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] -name = "openapi-spec-validator" -version = "0.7.2" +name = "pathable" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "lazy-object-proxy" }, - { name = "openapi-schema-validator" }, +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +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" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } + +[[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/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, + { 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 = "packaging" -version = "25.0" +name = "platformdirs" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] [[package]] -name = "parse" -version = "1.20.2" +name = "pluggy" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, + { 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 = "pathable" -version = "0.4.4" +name = "py-key-value-aio" +version = "0.2.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/35/65310a4818acec0f87a46e5565e341c5a96fc062a9a03495ad28828ff4d7/py_key_value_aio-0.2.8.tar.gz", hash = "sha256:c0cfbb0bd4e962a3fa1a9fa6db9ba9df812899bd9312fa6368aaea7b26008b36", size = 32853, upload-time = "2025-10-24T13:31:04.688Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/e56747d87a97ad2aff0f3700d77f186f0704c90c2da03bfed9e113dae284/py_key_value_aio-0.2.8-py3-none-any.whl", hash = "sha256:561565547ce8162128fd2bd0b9d70ce04a5f4586da8500cce79a54dfac78c46a", size = 69200, upload-time = "2025-10-24T13:31:03.81Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, ] [[package]] -name = "pluggy" -version = "1.6.0" +name = "py-key-value-shared" +version = "0.2.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/79/05a1f9280cfa0709479319cbfd2b1c5beb23d5034624f548c83fb65b0b61/py_key_value_shared-0.2.8.tar.gz", hash = "sha256:703b4d3c61af124f0d528ba85995c3c8d78f8bd3d2b217377bd3278598070cc1", size = 8216, upload-time = "2025-10-24T13:31:03.601Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/84/7a/1726ceaa3343874f322dd83c9ec376ad81f533df8422b8b1e1233a59f8ce/py_key_value_shared-0.2.8-py3-none-any.whl", hash = "sha256:aff1bbfd46d065b2d67897d298642e80e5349eae588c6d11b48452b46b8d46ba", size = 14586, upload-time = "2025-10-24T13:31:02.838Z" }, ] [[package]] @@ -998,6 +972,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyperclip" version = "1.11.0" @@ -1079,6 +1067,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1172,18 +1169,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - [[package]] name = "rich" version = "13.9.4" @@ -1347,12 +1332,16 @@ wheels = [ ] [[package]] -name = "six" -version = "1.17.0" +name = "secretstorage" +version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, ] [[package]] @@ -1483,13 +1472,69 @@ wheels = [ ] [[package]] -name = "werkzeug" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { 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 = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] diff --git a/README-zh.md b/README-zh.md index 9a61bfe7..df84bfa1 100644 --- a/README-zh.md +++ b/README-zh.md @@ -38,17 +38,42 @@ MCP for Unity 作为桥梁,允许 AI 助手(如 Claude、Cursor)通过本 您的大语言模型可以使用以下功能: - * `read_console`: 获取控制台消息或清除控制台。 - * `manage_script`: 管理 C# 脚本(创建、读取、更新、删除)。 - * `manage_editor`: 控制和查询编辑器的状态和设置。 - * `manage_scene`: 管理场景(加载、保存、创建、获取层次结构等)。 - * `manage_asset`: 执行资源操作(导入、创建、修改、删除等)。 - * `manage_shader`: 执行着色器 CRUD 操作(创建、读取、修改、删除)。 - * `manage_gameobject`: 管理游戏对象:创建、修改、删除、查找和组件操作。 - * `execute_menu_item`: 执行 Unity 编辑器菜单项(例如,执行"File/Save Project")。 - * `apply_text_edits`: 具有前置条件哈希和原子多编辑批次的精确文本编辑。 - * `script_apply_edits`: 结构化 C# 方法/类编辑(插入/替换/删除),具有更安全的边界。 - * `validate_script`: 快速验证(基本/标准)以在写入前后捕获语法/结构问题。 +* `execute_menu_item`: 执行 Unity 编辑器菜单项(例如,"File/Save Project")。 +* `manage_asset`: 执行资源操作(导入、创建、修改、删除等)。 +* `manage_editor`: 控制和查询编辑器的状态和设置。 +* `manage_gameobject`: 管理游戏对象:创建、修改、删除、查找和组件操作。 +* `manage_prefabs`: 执行预制件操作(创建、修改、删除等)。 +* `manage_scene`: 管理场景(加载、保存、创建、获取层次结构等)。 +* `manage_script`: 传统脚本操作的兼容性路由器(创建、读取、删除)。建议使用 `apply_text_edits` 或 `script_apply_edits` 进行编辑。 +* `manage_shader`: 执行着色器 CRUD 操作(创建、读取、修改、删除)。 +* `read_console`: 获取控制台消息或清除控制台。 +* `run_tests`: 在 Unity 编辑器中运行测试。 +* `set_active_instance`: 将后续工具调用路由到特定的 Unity 实例(当运行多个实例时)。 +* `apply_text_edits`: 具有前置条件哈希和原子多编辑批次的精确文本编辑。 +* `script_apply_edits`: 结构化 C# 方法/类编辑(插入/替换/删除),具有更安全的边界。 +* `validate_script`: 快速验证(基本/标准)以在写入前后捕获语法/结构问题。 +* `create_script`: 在给定的项目路径创建新的 C# 脚本。 +* `delete_script`: 通过 URI 或 Assets 相对路径删除 C# 脚本。 +* `get_sha`: 获取 Unity C# 脚本的 SHA256 和基本元数据,而不返回文件内容。 + + + +
+ 可用资源 + + 您的大语言模型可以检索以下资源: + +* `unity_instances`: 列出所有正在运行的 Unity 编辑器实例及其详细信息(名称、路径、端口、状态)。 +* `menu_items`: 检索 Unity 编辑器中所有可用的菜单项。 +* `tests`: 检索 Unity 编辑器中所有可用的测试。可以选择特定类型的测试(例如,"EditMode"、"PlayMode")。 +* `editor_active_tool`: 当前活动的编辑器工具(移动、旋转、缩放等)和变换手柄设置。 +* `editor_prefab_stage`: 如果预制件在隔离模式下打开,则为当前预制件编辑上下文。 +* `editor_selection`: 有关编辑器中当前选定对象的详细信息。 +* `editor_state`: 当前编辑器运行时状态,包括播放模式、编译状态、活动场景和选择摘要。 +* `editor_windows`: 所有当前打开的编辑器窗口及其标题、类型、位置和焦点状态。 +* `project_info`: 静态项目信息,包括根路径、Unity 版本和平台。 +* `project_layers`: 项目 TagManager 中定义的所有层及其索引(0-31)。 +* `project_tags`: 项目 TagManager 中定义的所有标签。
--- diff --git a/README.md b/README.md index 3bfb267e..6f8a255e 100644 --- a/README.md +++ b/README.md @@ -40,19 +40,23 @@ MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to Your LLM can use functions like: - * `read_console`: Gets messages from or clears the console. - * `manage_script`: Manages C# scripts (create, read, update, delete). - * `manage_editor`: Controls and queries the editor's state and settings. - * `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.). - * `manage_asset`: Performs asset operations (import, create, modify, delete, etc.). - * `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). - * `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations. - * `execute_menu_item`: Executes Unity Editor menu items (e.g., "File/Save Project"). - * `apply_text_edits`: Precise text edits with precondition hashes and atomic multi-edit batches. - * `script_apply_edits`: Structured C# method/class edits (insert/replace/delete) with safer boundaries. - * `validate_script`: Fast validation (basic/standard) to catch syntax/structure issues before/after writes. - * `run_test`: Runs a tests in the Unity Editor. - * `set_active_instance`: Routes subsequent tool calls to a specific Unity instance (when multiple are running). +* `execute_menu_item`: Executes Unity Editor menu items (e.g., "File/Save Project"). +* `manage_asset`: Performs asset operations (import, create, modify, delete, etc.). +* `manage_editor`: Controls and queries the editor's state and settings. +* `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations. +* `manage_prefabs`: Performs prefab operations (create, modify, delete, etc.). +* `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.). +* `manage_script`: Compatibility router for legacy script operations (create, read, delete). Prefer `apply_text_edits` or `script_apply_edits` for edits. +* `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). +* `read_console`: Gets messages from or clears the console. +* `run_tests`: Runs tests in the Unity Editor. +* `set_active_instance`: Routes subsequent tool calls to a specific Unity instance (when multiple are running). +* `apply_text_edits`: Precise text edits with precondition hashes and atomic multi-edit batches. +* `script_apply_edits`: Structured C# method/class edits (insert/replace/delete) with safer boundaries. +* `validate_script`: Fast validation (basic/standard) to catch syntax/structure issues before/after writes. +* `create_script`: Create a new C# script at the given project path. +* `delete_script`: Delete a C# script by URI or Assets-relative path. +* `get_sha`: Get SHA256 and basic metadata for a Unity C# script without returning file contents. @@ -61,9 +65,17 @@ MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to Your LLM can retrieve the following resources: - * `unity_instances`: Lists all running Unity Editor instances with their details (name, path, port, status). - * `menu_items`: Retrieves all available menu items in the Unity Editor. - * `tests`: Retrieves all available tests in the Unity Editor. Can select tests of a specific type (e.g., "EditMode", "PlayMode"). +* `unity_instances`: Lists all running Unity Editor instances with their details (name, path, port, status). +* `menu_items`: Retrieves all available menu items in the Unity Editor. +* `tests`: Retrieves all available tests in the Unity Editor. Can select tests of a specific type (e.g., "EditMode", "PlayMode"). +* `editor_active_tool`: Currently active editor tool (Move, Rotate, Scale, etc.) and transform handle settings. +* `editor_prefab_stage`: Current prefab editing context if a prefab is open in isolation mode. +* `editor_selection`: Detailed information about currently selected objects in the editor. +* `editor_state`: Current editor runtime state including play mode, compilation status, active scene, and selection summary. +* `editor_windows`: All currently open editor windows with their titles, types, positions, and focus state. +* `project_info`: Static project information including root path, Unity version, and platform. +* `project_layers`: All layers defined in the project's TagManager with their indices (0-31). +* `project_tags`: All tags defined in the project's TagManager. --- diff --git a/Server/resources/unity_instances.py b/Server/resources/unity_instances.py index 0d2df784..c716ea35 100644 --- a/Server/resources/unity_instances.py +++ b/Server/resources/unity_instances.py @@ -13,7 +13,7 @@ name="unity_instances", description="Lists all running Unity Editor instances with their details." ) -def unity_instances(ctx: Context) -> dict[str, Any]: +async def unity_instances(ctx: Context) -> dict[str, Any]: """ List all available Unity Editor instances. @@ -30,7 +30,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]: Returns: Dictionary containing list of instances and metadata """ - ctx.info("Listing Unity instances") + await ctx.info("Listing Unity instances") try: pool = get_unity_connection_pool() @@ -58,7 +58,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]: return result except Exception as e: - ctx.error(f"Error listing Unity instances: {e}") + await ctx.error(f"Error listing Unity instances: {e}") return { "success": False, "error": f"Failed to list Unity instances: {str(e)}", diff --git a/Server/server.py b/Server/server.py index 48c33ff4..c491e93d 100644 --- a/Server/server.py +++ b/Server/server.py @@ -108,12 +108,14 @@ def _emit_startup(): instances = _unity_connection_pool.discover_all_instances() if instances: - logger.info(f"Discovered {len(instances)} Unity instance(s): {[i.id for i in instances]}") + logger.info( + f"Discovered {len(instances)} Unity instance(s): {[i.id for i in instances]}") # Try to connect to default instance try: _unity_connection_pool.get_connection() - logger.info("Connected to default Unity instance on startup") + logger.info( + "Connected to default Unity instance on startup") # Record successful Unity connection (deferred) import threading as _t @@ -126,7 +128,8 @@ def _emit_startup(): } )).start() except Exception as e: - logger.warning("Could not connect to default Unity instance: %s", e) + logger.warning( + "Could not connect to default Unity instance: %s", e) else: logger.warning("No Unity instances found on startup") @@ -172,23 +175,31 @@ def _emit_startup(): name="mcp-for-unity-server", lifespan=server_lifespan, instructions=""" -This server provides tools to interact with the Unity Game Engine Editor.\n\n -Available tools:\n -- `manage_editor`: Controls editor state and queries info.\n -- `execute_menu_item`: Executes, lists and checks for the existence of Unity Editor menu items.\n -- `read_console`: Reads or clears Unity console messages, with filtering options.\n -- `manage_scene`: Manages scenes.\n -- `manage_gameobject`: Manages GameObjects in the scene.\n -- `manage_script`: Manages C# script files.\n -- `manage_asset`: Manages prefabs and assets.\n -- `manage_shader`: Manages shaders.\n\n -- Tips:\n -- Create prefabs for reusable GameObjects.\n -- Always include a camera and main light in your scenes.\n -- Unless specified otherwise, paths are relative to the project's `Assets/` folder.\n -- After creating or modifying scripts with `manage_script`, allow Unity to recompile; use `read_console` to check for compile errors.\n -- Use `execute_menu_item` for interacting with Unity systems and third party tools like a user would.\n +This server provides tools to interact with the Unity Game Engine Editor. +Important Workflows: + +Script Management: +1. After creating or modifying scripts with `manage_script` +2. Use `read_console` to check for compilation errors before proceeding +3. Only after successful compilation can new components/types be used + +Scene Setup: +- Always include a Camera and main Light (Directional Light) in new scenes +- Create prefabs with `manage_asset` for reusable GameObjects +- Use `manage_scene` to load, save, and query scene information + +Path Conventions: +- Unless specified otherwise, all paths are relative to the project's `Assets/` folder +- Use forward slashes (/) in paths for cross-platform compatibility + +Console Monitoring: +- Check `read_console` regularly to catch errors, warnings, and compilation status +- Filter by log type (Error, Warning, Log) to focus on specific issues + +Menu Items: +- Use `execute_menu_item` when you have read the menu items resource +- This lets you interact with Unity's menu system and third-party tools """ ) @@ -237,7 +248,8 @@ def main(): # Set environment variable if --default-instance is provided if args.default_instance: os.environ["UNITY_MCP_DEFAULT_INSTANCE"] = args.default_instance - logger.info(f"Using default Unity instance from command-line: {args.default_instance}") + logger.info( + f"Using default Unity instance from command-line: {args.default_instance}") mcp.run(transport='stdio') diff --git a/Server/tools/run_tests.py b/Server/tools/run_tests.py index 35234a53..7d5a89a3 100644 --- a/Server/tools/run_tests.py +++ b/Server/tools/run_tests.py @@ -44,11 +44,11 @@ class RunTestsResponse(MCPResponse): ) async def run_tests( ctx: Context, - mode: Annotated[Literal["edit", "play"], "Unity test mode to run"] = "edit", - timeout_seconds: Annotated[int | str | None, "Optional timeout in seconds for the Unity test run (string, e.g. '30')"] = None, -) -> dict[str, Any]: - # Get active instance from session state - # Removed session_state import + mode: Annotated[Literal["EditMode", "PlayMode"], Field( + description="Unity test mode to run")] = "EditMode", + timeout_seconds: Annotated[int | str, Field( + description="Optional timeout in seconds for the Unity test run (string, e.g. '30')")] | None = None, +) -> RunTestsResponse: unity_instance = get_unity_instance_from_context(ctx) # Coerce timeout defensively (string/float -> int) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs index e18c014d..96ad9469 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPToolParameterTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; -using System.Collections; using UnityEditor; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Tools; diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs index e52c7d0b..4512d919 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using NUnit.Framework; using UnityEngine; using MCPForUnity.Editor.Tools; -using static MCPForUnity.Editor.Tools.ManageGameObject; namespace MCPForUnityTests.Editor.Tools { diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs index ed8ef3c6..1637a5b4 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NUnit.Framework; using MCPForUnity.Editor.Tools; diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs index 5ab03e80..e9af2580 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs @@ -2,7 +2,6 @@ using NUnit.Framework; using UnityEngine; using MCPForUnity.Editor.Tools; -using static MCPForUnity.Editor.Tools.ManageGameObject; namespace MCPForUnityTests.Editor.Tools { diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs index 0df26d34..ee05c0c3 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections; using NUnit.Framework; using UnityEngine; -using UnityEditor; using UnityEngine.TestTools; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Tools; diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs index 44288457..a562f667 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs @@ -5,7 +5,6 @@ using UnityEditor.SceneManagement; using UnityEngine; using MCPForUnity.Editor.Tools.Prefabs; -using MCPForUnity.Editor.Tools; namespace MCPForUnityTests.Editor.Tools { @@ -53,11 +52,11 @@ public void OpenStage_OpensPrefabInIsolation() Assert.IsTrue(openResult.Value("success"), "open_stage should succeed for a valid prefab."); - PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + UnityEditor.SceneManagement.PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); Assert.IsNotNull(stage, "Prefab stage should be open after open_stage."); Assert.AreEqual(prefabPath, stage.assetPath, "Opened stage should match prefab path."); - var stageInfo = ToJObject(ManageEditor.HandleCommand(new JObject { ["action"] = "get_prefab_stage" })); + var stageInfo = ToJObject(MCPForUnity.Editor.Resources.Editor.PrefabStage.HandleCommand(new JObject())); Assert.IsTrue(stageInfo.Value("success"), "get_prefab_stage should succeed when stage is open."); var data = stageInfo["data"] as JObject; @@ -125,7 +124,7 @@ public void SaveOpenStage_SavesDirtyChanges() ["prefabPath"] = prefabPath }); - PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); + UnityEditor.SceneManagement.PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); Assert.IsNotNull(stage, "Stage should be open before modifying."); stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f); diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialMeshInstantiationTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialMeshInstantiationTests.cs index fb453ddc..7ff9c903 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialMeshInstantiationTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialMeshInstantiationTests.cs @@ -1,10 +1,6 @@ -using System; using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using UnityEngine; -using UnityEditor; -using UnityEngine.TestTools; using MCPForUnity.Editor.Helpers; namespace MCPForUnityTests.Editor.Tools diff --git a/docs/CUSTOM_TOOLS.md b/docs/CUSTOM_TOOLS.md index 5c9ef9ad..4a293b40 100644 --- a/docs/CUSTOM_TOOLS.md +++ b/docs/CUSTOM_TOOLS.md @@ -29,7 +29,7 @@ Create a Python file **anywhere in your Unity project**. For example, `Assets/Ed ```python from typing import Annotated, Any -from mcp.server.fastmcp import Context +from fastmcp import Context from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry @@ -127,7 +127,7 @@ Here's a complete example showing how to create a screenshot capture tool. ```python from typing import Annotated, Any -from mcp.server.fastmcp import Context +from fastmcp import Context from registry import mcp_for_unity_tool from unity_connection import send_command_with_retry diff --git a/docs/README-DEV.md b/docs/README-DEV.md index 9c1681d1..85e3c271 100644 --- a/docs/README-DEV.md +++ b/docs/README-DEV.md @@ -9,32 +9,44 @@ Welcome to the MCP for Unity development environment! This directory contains to ### Installing Development Dependencies -To contribute or run tests, you need to install the development dependencies: +To contribute or run tests, you need to install the development dependencies using `uv`: ```bash # Navigate to the server source directory cd MCPForUnity/UnityMcpServer~/src # Install the package in editable mode with dev dependencies -pip install -e .[dev] +uv pip install -e ".[dev]" ``` This installs: -- **Runtime dependencies**: `httpx`, `mcp`, `pydantic`, `tomli` -- **Development dependencies**: `pytest`, `pytest-anyio` +- **Runtime dependencies**: `httpx`, `fastmcp`, `mcp`, `pydantic`, `tomli` +- **Development dependencies**: `pytest`, `pytest-asyncio` ### Running Tests ```bash -# From the repo root -pytest tests/ -v +# From the server source directory +cd MCPForUnity/UnityMcpServer~/src +uv run pytest tests/ -v ``` -Or if you prefer using Python module syntax: +Or from the repo root: + +```bash +# Using uv from the server directory +cd MCPForUnity/UnityMcpServer~/src && uv run pytest tests/ -v +``` + +To run only integration tests: +```bash +uv run pytest tests/ -v -m integration +``` +To run only unit tests: ```bash -python -m pytest tests/ -v +uv run pytest tests/ -v -m unit ``` ## 🚀 Available Development Features diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index b287405b..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -testpaths = tests -norecursedirs = UnityMcpBridge MCPForUnity - diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 7c25bfae..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,28 +0,0 @@ -import os - -# Ensure telemetry is disabled during test collection and execution to avoid -# any background network or thread startup that could slow or block pytest. -os.environ.setdefault("DISABLE_TELEMETRY", "true") -os.environ.setdefault("UNITY_MCP_DISABLE_TELEMETRY", "true") -os.environ.setdefault("MCP_DISABLE_TELEMETRY", "true") - -# Avoid collecting tests under the two 'src' package folders to prevent -# duplicate-package import conflicts (two different 'src' packages). -collect_ignore = [ - "UnityMcpBridge/UnityMcpServer~/src", - "MCPForUnity/UnityMcpServer~/src", -] -collect_ignore_glob = [ - "UnityMcpBridge/UnityMcpServer~/src/*", - "MCPForUnity/UnityMcpServer~/src/*", -] - -def pytest_ignore_collect(path): - p = str(path) - norm = p.replace("\\", "/") - return ( - "/UnityMcpBridge/UnityMcpServer~/src/" in norm - or "/MCPForUnity/UnityMcpServer~/src/" in norm - or norm.endswith("UnityMcpBridge/UnityMcpServer~/src") - or norm.endswith("MCPForUnity/UnityMcpServer~/src") - ) diff --git a/tests/test_manage_asset_param_coercion.py b/tests/test_manage_asset_param_coercion.py deleted file mode 100644 index 28fecb8b..00000000 --- a/tests/test_manage_asset_param_coercion.py +++ /dev/null @@ -1,73 +0,0 @@ -import sys -import pathlib -import importlib.util -import types -import asyncio -import os - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def _load_module(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Cannot load module {name} from {path}") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -# Stub fastmcp to avoid real MCP deps -fastmcp_pkg = types.ModuleType("fastmcp") - - -class _Dummy: - pass - - -fastmcp_pkg.FastMCP = _Dummy -fastmcp_pkg.Context = _Dummy -sys.modules.setdefault("fastmcp", fastmcp_pkg) - - -from tests.test_helpers import DummyContext - - -def test_manage_asset_pagination_coercion(monkeypatch): - # Import with SRC as CWD to satisfy telemetry import side effects - _prev = os.getcwd() - os.chdir(str(SRC)) - try: - manage_asset_mod = _load_module(SRC / "tools" / "manage_asset.py", "manage_asset_mod") - finally: - os.chdir(_prev) - - captured = {} - - async def fake_async_send(cmd, params, **kwargs): - captured["params"] = params - return {"success": True, "data": {}} - - monkeypatch.setattr(manage_asset_mod, "async_send_command_with_retry", fake_async_send) - - result = asyncio.run( - manage_asset_mod.manage_asset( - ctx=DummyContext(), - action="search", - path="Assets", - page_size="50", - page_number="2", - ) - ) - - assert result == {"success": True, "data": {}} - assert captured["params"]["pageSize"] == 50 - assert captured["params"]["pageNumber"] == 2 - - - - - - diff --git a/tests/test_manage_gameobject_param_coercion.py b/tests/test_manage_gameobject_param_coercion.py deleted file mode 100644 index d940b494..00000000 --- a/tests/test_manage_gameobject_param_coercion.py +++ /dev/null @@ -1,71 +0,0 @@ -import sys -import pathlib -import importlib.util -import types -import os - -ROOT = pathlib.Path(__file__).resolve().parents[1] -SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src" -sys.path.insert(0, str(SRC)) - - -def _load_module(path: pathlib.Path, name: str): - spec = importlib.util.spec_from_file_location(name, path) - if spec is None or spec.loader is None: - raise ImportError(f"Cannot load module {name} from {path}") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -# Stub fastmcp to avoid real MCP deps -fastmcp_pkg = types.ModuleType("fastmcp") - - -class _Dummy: - pass - - -fastmcp_pkg.FastMCP = _Dummy -fastmcp_pkg.Context = _Dummy -sys.modules.setdefault("fastmcp", fastmcp_pkg) - - -from tests.test_helpers import DummyContext - - -def test_manage_gameobject_boolean_and_tag_mapping(monkeypatch): - # Import with SRC as CWD to satisfy telemetry import side effects - _prev = os.getcwd() - os.chdir(str(SRC)) - try: - manage_go_mod = _load_module(SRC / "tools" / "manage_gameobject.py", "manage_go_mod") - finally: - os.chdir(_prev) - - captured = {} - - def fake_send(cmd, params): - captured["params"] = params - return {"success": True, "data": {}} - - monkeypatch.setattr(manage_go_mod, "send_command_with_retry", fake_send) - - # find by tag: allow tag to map to searchTerm - resp = manage_go_mod.manage_gameobject( - ctx=DummyContext(), - action="find", - search_method="by_tag", - tag="Player", - find_all="true", - search_inactive="0", - ) - # Loosen equality: wrapper may include a diagnostic message - assert resp.get("success") is True - assert "data" in resp - # ensure tag mapped to searchTerm and booleans passed through; C# side coerces true/false already - assert captured["params"]["searchTerm"] == "Player" - assert captured["params"]["findAll"] == "true" or captured["params"]["findAll"] is True - assert captured["params"]["searchInactive"] in ("0", False, 0) - -