Move Get commands to editor resources + Run Python tests every update (#368)

* Add a function to reload the domain

Closes #357

* feat: restructure server instructions into workflow-focused format

- Reorganized instructions from flat bullet list into categorized workflow sections
- Emphasized critical script management workflow with numbered steps
- Improved readability and scannability for AI agents using the MCP server

It doesn't make sense to repeat the fucnction tools, they're already parsed

* docs: reorder tool list alphabetically in README + add reload_domain tool

* feat: add Unity editor state and project info resources

- Implemented resources for querying active tool, editor state, prefab stage, selection, and open windows
- Added project configuration resources for layers and project metadata
- Organized new resources into Editor and Project namespaces for better structure

* feat: clarify script management workflow in system prompt

- Expanded guidance to include scripts created by any tool, not just manage_script
- Added "etc" to tools examples for better clarity

* refactor: remove reload_domain tool and update script management workflow

- Removed reload_domain tool as Unity automatically recompiles scripts when modified
- Updated script management instructions to rely on editor_state polling and console checking instead of manual domain reload
- Simplified workflow by removing unnecessary manual recompilation step

* Change name of menu items resource as the LLM seems it

* refactor: reorganize tests into src/tests/integration directory

- Moved all test files from root tests/ to MCPForUnity/UnityMcpServer~/src/tests/integration/ for better organization
- Added conftest.py with telemetry and dependency stubs to simplify test setup
- Removed redundant path manipulation and module loading code from individual test files

* feat: expand Unity test workflow triggers

- Run tests on all branches instead of only main
- Add pull request trigger to catch issues before merge
- Maintain path filtering to run only when relevant files change

* chore: add GitHub Actions workflow for Python tests

- Configured automated testing on push and pull requests using pytest
- Set up uv for dependency management and Python 3.10 environment
- Added test results artifact upload for debugging failed runs

* refactor: update import path for fastmcp Context

* docs: update development setup instructions to use uv

- Changed installation commands from pip to uv pip for better dependency management
- Updated test running instructions to use uv run pytest
- Added examples for running integration and unit tests separately

* Formatting [skip ci]

* refactor: optimize CI workflow with path filters and dependency installation

- Added path filters to only trigger tests when Python source or workflow files change
- Split dependency installation into sync and dev install steps for better clarity
- Fixed YAML indentation for improved readability

* Update .github/workflows/python-tests.yml

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: standardize test mode values to match Unity's naming convention

- Changed default mode from "edit" to "EditMode" in C# code
- Updated Python tool to use "EditMode" and "PlayMode" instead of lowercase variants

* refactor: convert test imports to relative imports

- Changed absolute imports to relative imports in integration tests for better package structure
- Removed test packages from pyproject.toml package list

* refactor: use Field with default_factory for mutable default in TagsResponse

* refactor: remove duplicate PrefabStageUtility call

* Update this as well [skip ci]

* Update MCPForUnity/UnityMcpServer~/src/tests/integration/test_script_tools.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: remove pull_request triggers from test workflows [skip ci]

It's already covered by pushes

* refactor: update resource function return types to include MCPResponse union

* refactor: remove manual domain reload tool

- Removed reload_domain tool as Unity handles script recompilation automatically
- Updated documentation to reflect automatic compilation workflow
- Simplified script management workflow instructions in server description

* refactor: add context support to resource handlers

- Updated all resource handlers to accept Context parameter for Unity instance routing
- Replaced direct async_send_command_with_retry calls with async_send_with_unity_instance wrapper
- Added imports for get_unity_instance_from_context and async_send_with_unity_instance helpers

* fix: correct grammar in menu items documentation

* docs: update README with expanded tools and resources documentation

- Added new tools: manage_prefabs, create_script, delete_script, get_sha
- Added new resources: editor state, windows, project info, layers, and tags
- Clarified manage_script as compatibility router with recommendation to use newer edit tools
- Fixed run_test to run_tests for consistency

* refactor: convert unity_instances function to async [skip ci]

- Changed function signature from synchronous to async
- Added await keywords to ctx.info() and ctx.error() calls to properly handle async context methods

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
main
Marcus Sanatan 2025-11-05 16:06:48 -04:00 committed by GitHub
parent f667582505
commit 2649d9c379
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 1434 additions and 1203 deletions

45
.github/workflows/python-tests.yml vendored Normal file
View File

@ -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/

View File

@ -3,7 +3,7 @@ name: Unity Tests
on: on:
workflow_dispatch: {} workflow_dispatch: {}
push: push:
branches: [main] branches: ["**"]
paths: paths:
- TestProjects/UnityMCPTests/** - TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/** - MCPForUnity/Editor/**

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: bacdb2f03a45d448888245e6ac9cca1b guid: 266967ec2e1df44209bf46ec6037d61d
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@ -0,0 +1,64 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides information about the currently active editor tool.
/// </summary>
[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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6e78b6227ab7742a8a4f679ee6a8a212
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,40 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides dynamic editor state information that changes frequently.
/// </summary>
[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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7c6df54e014c44fdb0cd3f65a479e37
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,42 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor.SceneManagement;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides information about the current prefab editing context.
/// </summary>
[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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a30b083e68bd4ae3b3d1ce5a45a9414
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,52 @@
using System;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides detailed information about the current editor selection.
/// </summary>
[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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7ea869623e094599a70be086ab4fc0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
{
/// <summary>
/// Provides list of all open editor windows.
/// </summary>
[McpForUnityResource("get_windows")]
public static class Windows
{
public static object HandleCommand(JObject @params)
{
try
{
EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll<EditorWindow>();
var openWindows = new List<object>();
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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 58a341e64bea440b29deaf859aaea552
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 538489f13d7914c4eba9a67e29001b43
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
{
/// <summary>
/// Provides dictionary of layer indices to layer names.
/// </summary>
[McpForUnityResource("get_layers")]
public static class Layers
{
private const int TotalLayerCount = 32;
public static object HandleCommand(JObject @params)
{
try
{
var layers = new Dictionary<int, string>();
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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 959ee428299454ac19a636275208ca00
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
{
/// <summary>
/// Provides static project configuration information.
/// </summary>
[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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81b03415fcf93466e9ed667d19b58d43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,27 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditorInternal;
namespace MCPForUnity.Editor.Resources.Project
{
/// <summary>
/// Provides list of all tags in the project.
/// </summary>
[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}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2179ac5d98f264d1681e7d5c0d0ed341
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -87,19 +87,19 @@ namespace MCPForUnity.Editor.Resources.Tests
return false; return false;
} }
if (modeStr.Equals("edit", StringComparison.OrdinalIgnoreCase)) if (modeStr.Equals("EditMode", StringComparison.OrdinalIgnoreCase))
{ {
mode = TestMode.EditMode; mode = TestMode.EditMode;
return true; return true;
} }
if (modeStr.Equals("play", StringComparison.OrdinalIgnoreCase)) if (modeStr.Equals("PlayMode", StringComparison.OrdinalIgnoreCase))
{ {
mode = TestMode.PlayMode; mode = TestMode.PlayMode;
return true; return true;
} }
error = $"Unknown test mode: '{modeStr}'. Use 'edit' or 'play'"; error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'";
return false; return false;
} }
} }

View File

@ -1,19 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using UnityEditor; using UnityEditor;
using UnityEditorInternal; // Required for tag management using UnityEditorInternal; // Required for tag management
using UnityEditor.SceneManagement;
using UnityEngine;
using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools namespace MCPForUnity.Editor.Tools
{ {
/// <summary> /// <summary>
/// Handles operations related to controlling and querying the Unity Editor state, /// Handles editor control actions including play mode control, tool selection,
/// including managing Tags and Layers. /// and tag/layer management. For reading editor state, use MCP resources instead.
/// </summary> /// </summary>
[McpForUnityTool("manage_editor")] [McpForUnityTool("manage_editor")]
public static class ManageEditor public static class ManageEditor
@ -89,19 +84,7 @@ namespace MCPForUnity.Editor.Tools
return Response.Error($"Error stopping play mode: {e.Message}"); return Response.Error($"Error stopping play mode: {e.Message}");
} }
// Editor State/Info // Tool Control
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();
case "set_active_tool": case "set_active_tool":
string toolName = @params["toolName"]?.ToString(); string toolName = @params["toolName"]?.ToString();
if (string.IsNullOrEmpty(toolName)) if (string.IsNullOrEmpty(toolName))
@ -117,9 +100,6 @@ namespace MCPForUnity.Editor.Tools
if (string.IsNullOrEmpty(tagName)) if (string.IsNullOrEmpty(tagName))
return Response.Error("'tagName' parameter required for remove_tag."); return Response.Error("'tagName' parameter required for remove_tag.");
return RemoveTag(tagName); return RemoveTag(tagName);
case "get_tags":
return GetTags(); // Helper to list current tags
// Layer Management // Layer Management
case "add_layer": case "add_layer":
if (string.IsNullOrEmpty(layerName)) if (string.IsNullOrEmpty(layerName))
@ -129,9 +109,6 @@ namespace MCPForUnity.Editor.Tools
if (string.IsNullOrEmpty(layerName)) if (string.IsNullOrEmpty(layerName))
return Response.Error("'layerName' parameter required for remove_layer."); return Response.Error("'layerName' parameter required for remove_layer.");
return RemoveLayer(layerName); return RemoveLayer(layerName);
case "get_layers":
return GetLayers(); // Helper to list current layers
// --- Settings (Example) --- // --- Settings (Example) ---
// case "set_resolution": // case "set_resolution":
// int? width = @params["width"]?.ToObject<int?>(); // int? width = @params["width"]?.ToObject<int?>();
@ -144,167 +121,12 @@ namespace MCPForUnity.Editor.Tools
default: default:
return Response.Error( 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 --- // --- Tool Control 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 <Project>/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<object>();
// Find currently open instances
// Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows
EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll<EditorWindow>();
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}");
}
}
private static object SetActiveTool(string toolName) private static object SetActiveTool(string toolName)
{ {
@ -341,43 +163,6 @@ namespace MCPForUnity.Editor.Tools
} }
} }
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 --- // --- Tag Management Methods ---
private static object AddTag(string tagName) private static object AddTag(string tagName)
@ -386,7 +171,7 @@ namespace MCPForUnity.Editor.Tools
return Response.Error("Tag name cannot be empty or whitespace."); return Response.Error("Tag name cannot be empty or whitespace.");
// Check if tag already exists // 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."); return Response.Error($"Tag '{tagName}' already exists.");
} }
@ -413,7 +198,7 @@ namespace MCPForUnity.Editor.Tools
return Response.Error("Cannot remove the built-in 'Untagged' tag."); return Response.Error("Cannot remove the built-in 'Untagged' tag.");
// Check if tag exists before attempting removal // 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."); return Response.Error($"Tag '{tagName}' does not exist.");
} }
@ -433,19 +218,6 @@ namespace MCPForUnity.Editor.Tools
} }
} }
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 --- // --- Layer Management Methods ---
private static object AddLayer(string layerName) private static object AddLayer(string layerName)
@ -569,27 +341,6 @@ namespace MCPForUnity.Editor.Tools
} }
} }
private static object GetLayers()
{
try
{
var layers = new Dictionary<int, string>();
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 --- // --- Helper Methods ---
/// <summary> /// <summary>
@ -605,7 +356,7 @@ namespace MCPForUnity.Editor.Tools
); );
if (tagManagerAssets == null || tagManagerAssets.Length == 0) 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; return null;
} }
// The first object in the asset file should be the TagManager // The first object in the asset file should be the TagManager
@ -613,7 +364,7 @@ namespace MCPForUnity.Editor.Tools
} }
catch (Exception e) catch (Exception e)
{ {
Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); McpLog.Error($"[ManageEditor] Error accessing TagManager.asset: {e.Message}");
return null; return null;
} }
} }
@ -624,22 +375,4 @@ namespace MCPForUnity.Editor.Tools
private static object SetQualityLevel(JToken qualityLevelToken) { ... } 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();
}
}
} }

View File

@ -20,7 +20,7 @@ namespace MCPForUnity.Editor.Tools
string modeStr = @params?["mode"]?.ToString(); string modeStr = @params?["mode"]?.ToString();
if (string.IsNullOrWhiteSpace(modeStr)) if (string.IsNullOrWhiteSpace(modeStr))
{ {
modeStr = "edit"; modeStr = "EditMode";
} }
if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError))

View File

@ -35,6 +35,7 @@ py-modules = [
"server", "server",
"telemetry", "telemetry",
"telemetry_decorator", "telemetry_decorator",
"unity_connection" "unity_connection",
"unity_instance_middleware"
] ]
packages = ["tools", "resources", "registry"] packages = ["tools", "resources", "registry"]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -12,10 +12,10 @@ class GetMenuItemsResponse(MCPResponse):
@mcp_for_unity_resource( @mcp_for_unity_resource(
uri="mcpforunity://menu-items", uri="mcpforunity://menu-items",
name="get_menu_items", name="menu_items",
description="Provides a list of all 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. """Provides a list of all menu items.
""" """
unity_instance = get_unity_instance_from_context(ctx) unity_instance = get_unity_instance_from_context(ctx)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -21,7 +21,7 @@ class GetTestsResponse(MCPResponse):
@mcp_for_unity_resource(uri="mcpforunity://tests", name="get_tests", description="Provides a list of all tests.") @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. """Provides a list of all tests.
""" """
unity_instance = get_unity_instance_from_context(ctx) 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( async def get_tests_for_mode(
ctx: Context, ctx: Context,
mode: Annotated[Literal["EditMode", "PlayMode"], Field(description="The mode to filter tests by.")], 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. """Provides a list of tests for a specific mode.
Args: Args:

View File

@ -13,7 +13,7 @@ from unity_connection import get_unity_connection_pool
name="unity_instances", name="unity_instances",
description="Lists all running Unity Editor instances with their details." 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. List all available Unity Editor instances.
@ -30,7 +30,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]:
Returns: Returns:
Dictionary containing list of instances and metadata Dictionary containing list of instances and metadata
""" """
ctx.info("Listing Unity instances") await ctx.info("Listing Unity instances")
try: try:
pool = get_unity_connection_pool() pool = get_unity_connection_pool()
@ -58,7 +58,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]:
return result return result
except Exception as e: except Exception as e:
ctx.error(f"Error listing Unity instances: {e}") await ctx.error(f"Error listing Unity instances: {e}")
return { return {
"success": False, "success": False,
"error": f"Failed to list Unity instances: {str(e)}", "error": f"Failed to list Unity instances: {str(e)}",

View File

@ -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

View File

@ -172,23 +172,36 @@ mcp = FastMCP(
name="mcp-for-unity-server", name="mcp-for-unity-server",
lifespan=server_lifespan, lifespan=server_lifespan,
instructions=""" instructions="""
This server provides tools to interact with the Unity Game Engine Editor.\n\n This server provides tools to interact with the Unity Game Engine Editor.
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
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
""" """
) )

View File

@ -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)

View File

@ -1,23 +1,4 @@
import sys from .test_helpers import DummyContext
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")
class DummyMCP: class DummyMCP:
@ -28,9 +9,6 @@ class DummyMCP:
return deco return deco
from tests.test_helpers import DummyContext
def setup_tools(): def setup_tools():
mcp = DummyMCP() mcp = DummyMCP()
# Import the tools module to trigger decorator registration # Import the tools module to trigger decorator registration

View File

@ -1,53 +1,4 @@
import sys from .test_helpers import DummyContext
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)
class DummyMCP: class DummyMCP:

View File

@ -1,36 +1,7 @@
import sys
import pathlib
import importlib.util
import types
import asyncio import asyncio
import pytest import pytest
from tests.test_helpers import DummyContext from .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)
class DummyMCP: class DummyMCP:

View File

@ -1,24 +1,4 @@
import sys from .test_helpers import DummyContext
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")
class DummyMCP: class DummyMCP:
@ -32,9 +12,6 @@ class DummyMCP:
return deco return deco
from tests.test_helpers import DummyContext
def setup_tools(): def setup_tools():
mcp = DummyMCP() mcp = DummyMCP()
# Import the tools module to trigger decorator registration # Import the tools module to trigger decorator registration

View File

@ -2,25 +2,9 @@
Test the improved anchor matching logic. Test the improved anchor matching logic.
""" """
import sys import re
import pathlib
import importlib.util
# add server src to path and load modules import tools.script_apply_edits as script_apply_edits_module
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")
def test_improved_anchor_matching(): def test_improved_anchor_matching():
@ -41,8 +25,6 @@ public class TestClass : MonoBehaviour
} }
}''' }'''
import re
# Test the problematic anchor pattern # Test the problematic anchor pattern
anchor_pattern = r"\s*}\s*$" anchor_pattern = r"\s*}\s*$"
flags = re.MULTILINE flags = re.MULTILINE

View File

@ -10,18 +10,10 @@ DESIGN: Single source of truth via middleware state:
- get_unity_instance_from_context() reads from ctx.get_state() - get_unity_instance_from_context() reads from ctx.get_state()
- All tools (GameObject, Script, Asset, etc.) use get_unity_instance_from_context() - All tools (GameObject, Script, Asset, etc.) use get_unity_instance_from_context()
""" """
import sys
import pathlib
import pytest import pytest
from unittest.mock import AsyncMock, Mock, MagicMock, patch from unittest.mock import AsyncMock, Mock, MagicMock, patch
from fastmcp import Context 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 unity_instance_middleware import UnityInstanceMiddleware
from tools import get_unity_instance_from_context from tools import get_unity_instance_from_context

View File

@ -1,11 +1,4 @@
import sys from .test_helpers import DummyContext
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))
def test_manage_gameobject_uses_session_state(monkeypatch): def test_manage_gameobject_uses_session_state(monkeypatch):

View File

@ -4,7 +4,7 @@ Tests for JSON string parameter parsing in manage_asset tool.
import pytest import pytest
import json import json
from tests.test_helpers import DummyContext from .test_helpers import DummyContext
from tools.manage_asset import manage_asset from tools.manage_asset import manage_asset

View File

@ -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

View File

@ -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)

View File

@ -1,40 +1,6 @@
# import triggers registration elsewhere; no direct use here
import sys
import types
from pathlib import Path
import pytest import pytest
from .test_helpers import DummyContext
# 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
class DummyMCP: class DummyMCP:
@ -48,12 +14,6 @@ class DummyMCP:
return _decorator return _decorator
# (removed unused DummyCtx)
from tests.test_helpers import DummyContext
def _register_tools(): def _register_tools():
mcp = DummyMCP() mcp = DummyMCP()
# Import the tools module to trigger decorator registration # Import the tools module to trigger decorator registration

View File

@ -1,23 +1,4 @@
import sys from .test_helpers import DummyContext
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")
class DummyMCP: class DummyMCP:
@ -31,9 +12,6 @@ class DummyMCP:
return deco return deco
from tests.test_helpers import DummyContext
def setup_tools(): def setup_tools():
mcp = DummyMCP() mcp = DummyMCP()
# Import the tools module to trigger decorator registration # Import the tools module to trigger decorator registration

View File

@ -1,30 +1,7 @@
import sys
import pathlib
import asyncio import asyncio
import types
import pytest import pytest
ROOT = pathlib.Path(__file__).resolve().parents[1] from .test_helpers import DummyContext
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)
class DummyMCP: class DummyMCP:
@ -38,9 +15,6 @@ class DummyMCP:
return deco return deco
from tests.test_helpers import DummyContext
@pytest.fixture() @pytest.fixture()
def resource_tools(): def resource_tools():
mcp = DummyMCP() mcp = DummyMCP()

View File

@ -1,33 +1,6 @@
import sys
from pathlib import Path
import pytest import pytest
import types
# locate server src dynamically to avoid hardcoded layout assumptions from .test_helpers import DummyContext
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)
class DummyMCP: class DummyMCP:
@ -41,9 +14,6 @@ class DummyMCP:
return deco return deco
from tests.test_helpers import DummyContext
@pytest.fixture() @pytest.fixture()
def resource_tools(): def resource_tools():
mcp = DummyMCP() mcp = DummyMCP()

View File

@ -1,26 +1,7 @@
import sys
import pathlib
import importlib.util
import pytest import pytest
import asyncio import asyncio
# add server src to path and load modules without triggering package imports from .test_helpers import DummyContext
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")
class DummyMCP: class DummyMCP:
@ -34,9 +15,6 @@ class DummyMCP:
return decorator return decorator
from tests.test_helpers import DummyContext
def setup_manage_script(): def setup_manage_script():
mcp = DummyMCP() mcp = DummyMCP()
# Import the tools module to trigger decorator registration # Import the tools module to trigger decorator registration

View File

@ -7,14 +7,7 @@ def test_endpoint_rejects_non_http(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd") monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd")
# Import the telemetry module from the correct path # Import the telemetry module
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))
telemetry = importlib.import_module("telemetry") telemetry = importlib.import_module("telemetry")
importlib.reload(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) monkeypatch.delenv("UNITY_MCP_TELEMETRY_ENDPOINT", raising=False)
# Patch config.telemetry_endpoint via import mocking # 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") cfg_mod = importlib.import_module("config")
old_endpoint = cfg_mod.config.telemetry_endpoint old_endpoint = cfg_mod.config.telemetry_endpoint
cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry" cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry"
try: try:
monkeypatch.chdir(str(SRC))
telemetry = importlib.import_module("telemetry") telemetry = importlib.import_module("telemetry")
importlib.reload(telemetry) importlib.reload(telemetry)
tc = telemetry.TelemetryCollector() tc = telemetry.TelemetryCollector()
@ -50,7 +35,6 @@ def test_config_preferred_then_env_override(tmp_path, monkeypatch):
# Env should override config # Env should override config
monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT",
"https://override.example/ep") "https://override.example/ep")
monkeypatch.chdir(str(SRC))
importlib.reload(telemetry) importlib.reload(telemetry)
tc2 = telemetry.TelemetryCollector() tc2 = telemetry.TelemetryCollector()
assert tc2.config.endpoint == "https://override.example/ep" 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): def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
# Import the telemetry module from the correct path # Import the telemetry module
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))
telemetry = importlib.import_module("telemetry") telemetry = importlib.import_module("telemetry")
importlib.reload(telemetry) importlib.reload(telemetry)

View File

@ -1,60 +1,9 @@
import sys
import pathlib
import importlib.util
import os
import types import types
import threading import threading
import time import time
import queue as q import queue as q
import telemetry
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)
def test_telemetry_queue_backpressure_and_single_worker(monkeypatch, caplog): def test_telemetry_queue_backpressure_and_single_worker(monkeypatch, caplog):

View File

@ -6,10 +6,7 @@ def _get_decorator_module():
import sys import sys
import pathlib import pathlib
import types import types
ROOT = pathlib.Path(__file__).resolve().parents[1] # Tests can now import directly from parent package
SRC = ROOT / "MCPForUnity" / "UnityMcpServer~" / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
# Remove any previously stubbed module to force real import # Remove any previously stubbed module to force real import
sys.modules.pop("telemetry_decorator", None) sys.modules.pop("telemetry_decorator", None)
# Preload a minimal telemetry stub to satisfy telemetry_decorator imports # Preload a minimal telemetry stub to satisfy telemetry_decorator imports

View File

@ -23,7 +23,7 @@ if SRC is None:
"MCP for Unity server source not found. Tried:\n" + searched, "MCP for Unity server source not found. Tried:\n" + searched,
allow_module_level=True, 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): def start_dummy_server(greeting: bytes, respond_ping: bool = False):

View File

@ -1,23 +1,4 @@
import sys from .test_helpers import DummyContext
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")
class DummyMCP: class DummyMCP:
@ -31,9 +12,6 @@ class DummyMCP:
return deco return deco
from tests.test_helpers import DummyContext
def setup_tools(): def setup_tools():
mcp = DummyMCP() mcp = DummyMCP()
# Import the tools module to trigger decorator registration # Import the tools module to trigger decorator registration

View File

@ -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

View File

@ -44,11 +44,11 @@ class RunTestsResponse(MCPResponse):
) )
async def run_tests( async def run_tests(
ctx: Context, ctx: Context,
mode: Annotated[Literal["edit", "play"], "Unity test mode to run"] = "edit", mode: Annotated[Literal["EditMode", "PlayMode"], Field(
timeout_seconds: Annotated[int | str | None, "Optional timeout in seconds for the Unity test run (string, e.g. '30')"] = None, description="Unity test mode to run")] = "EditMode",
) -> dict[str, Any]: timeout_seconds: Annotated[int | str, Field(
# Get active instance from session state description="Optional timeout in seconds for the Unity test run (string, e.g. '30')")] | None = None,
# Removed session_state import ) -> RunTestsResponse:
unity_instance = get_unity_instance_from_context(ctx) unity_instance = get_unity_instance_from_context(ctx)
# Coerce timeout defensively (string/float -> int) # Coerce timeout defensively (string/float -> int)

View File

@ -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" }, { 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]] [[package]]
name = "certifi" name = "certifi"
version = "2025.1.31" 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" }, { 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]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.8.0" version = "2.8.0"
@ -393,24 +429,27 @@ wheels = [
[[package]] [[package]]
name = "fastmcp" name = "fastmcp"
version = "2.12.5" version = "2.13.0.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "authlib" }, { name = "authlib" },
{ name = "cyclopts" }, { name = "cyclopts" },
{ name = "exceptiongroup" }, { name = "exceptiongroup" },
{ name = "httpx" }, { name = "httpx" },
{ name = "jsonschema-path" },
{ name = "mcp" }, { name = "mcp" },
{ name = "openapi-core" },
{ name = "openapi-pydantic" }, { name = "openapi-pydantic" },
{ name = "platformdirs" },
{ name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] },
{ name = "pydantic", extra = ["email"] }, { name = "pydantic", extra = ["email"] },
{ name = "pyperclip" }, { name = "pyperclip" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "rich" }, { 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 = [ 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]] [[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" }, { 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]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@ -478,12 +529,48 @@ wheels = [
] ]
[[package]] [[package]]
name = "isodate" name = "jaraco-classes"
version = "0.7.2" version = "3.4.0"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/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/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]] [[package]]
@ -529,48 +616,21 @@ wheels = [
] ]
[[package]] [[package]]
name = "lazy-object-proxy" name = "keyring"
version = "1.12.0" version = "25.6.0"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" },
{ 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" },
] ]
[[package]] [[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" }, { 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]] [[package]]
name = "mcp" name = "mcp"
version = "1.16.0" version = "1.20.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
@ -681,15 +656,16 @@ dependencies = [
{ name = "jsonschema" }, { name = "jsonschema" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "pyjwt", extra = ["crypto"] },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette" }, { name = "sse-starlette" },
{ name = "starlette" }, { name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" }, { 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 = [ 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]] [[package]]
@ -712,7 +688,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "fastmcp", specifier = ">=2.12.5" }, { name = "fastmcp", specifier = ">=2.13.0" },
{ name = "httpx", specifier = ">=0.27.2" }, { name = "httpx", specifier = ">=0.27.2" },
{ name = "mcp", specifier = ">=1.16.0" }, { name = "mcp", specifier = ">=1.16.0" },
{ name = "pydantic", specifier = ">=2.12.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" }, { 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]] [[package]]
name = "openapi-pydantic" name = "openapi-pydantic"
version = "0.5.1" version = "0.5.1"
@ -772,35 +728,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
] ]
[[package]]
name = "openapi-schema-validator"
version = "0.6.3"
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" }
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" },
]
[[package]]
name = "openapi-spec-validator"
version = "0.7.2"
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/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
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" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@ -810,15 +737,6 @@ 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/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 = "parse"
version = "1.20.2"
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" }
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" },
]
[[package]] [[package]]
name = "pathable" name = "pathable"
version = "0.4.4" version = "0.4.4"
@ -828,6 +746,24 @@ 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/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
] ]
[[package]]
name = "pathvalidate"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.0"
source = { registry = "https://pypi.org/simple" }
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/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]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@ -837,6 +773,44 @@ 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/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 = "py-key-value-aio"
version = "0.2.8"
source = { registry = "https://pypi.org/simple" }
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/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 = "py-key-value-shared"
version = "0.2.8"
source = { registry = "https://pypi.org/simple" }
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/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]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.23" version = "2.23"
@ -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" }, { 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]] [[package]]
name = "pyperclip" name = "pyperclip"
version = "1.11.0" 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" }, { 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]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" 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" }, { 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]] [[package]]
name = "rich" name = "rich"
version = "13.9.4" version = "13.9.4"
@ -1347,12 +1332,16 @@ wheels = [
] ]
[[package]] [[package]]
name = "six" name = "secretstorage"
version = "1.17.0" version = "3.4.0"
source = { registry = "https://pypi.org/simple" } 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 = [ 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]] [[package]]
@ -1483,13 +1472,69 @@ wheels = [
] ]
[[package]] [[package]]
name = "werkzeug" name = "websockets"
version = "3.1.1" version = "15.0.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" }
{ 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 = [ 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" }, { 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" },
] ]

View File

@ -38,17 +38,42 @@ MCP for Unity 作为桥梁,允许 AI 助手(如 Claude、Cursor通过本
您的大语言模型可以使用以下功能: 您的大语言模型可以使用以下功能:
* `read_console`: 获取控制台消息或清除控制台。 * `execute_menu_item`: 执行 Unity 编辑器菜单项(例如,"File/Save Project")。
* `manage_script`: 管理 C# 脚本(创建、读取、更新、删除)。 * `manage_asset`: 执行资源操作(导入、创建、修改、删除等)。
* `manage_editor`: 控制和查询编辑器的状态和设置。 * `manage_editor`: 控制和查询编辑器的状态和设置。
* `manage_scene`: 管理场景(加载、保存、创建、获取层次结构等)。 * `manage_gameobject`: 管理游戏对象:创建、修改、删除、查找和组件操作。
* `manage_asset`: 执行资源操作(导入、创建、修改、删除等)。 * `manage_prefabs`: 执行预制件操作(创建、修改、删除等)。
* `manage_shader`: 执行着色器 CRUD 操作(创建、读取、修改、删除)。 * `manage_scene`: 管理场景(加载、保存、创建、获取层次结构等)。
* `manage_gameobject`: 管理游戏对象:创建、修改、删除、查找和组件操作。 * `manage_script`: 传统脚本操作的兼容性路由器(创建、读取、删除)。建议使用 `apply_text_edits``script_apply_edits` 进行编辑。
* `execute_menu_item`: 执行 Unity 编辑器菜单项(例如,执行"File/Save Project")。 * `manage_shader`: 执行着色器 CRUD 操作(创建、读取、修改、删除)。
* `apply_text_edits`: 具有前置条件哈希和原子多编辑批次的精确文本编辑。 * `read_console`: 获取控制台消息或清除控制台。
* `script_apply_edits`: 结构化 C# 方法/类编辑(插入/替换/删除),具有更安全的边界。 * `run_tests`: 在 Unity 编辑器中运行测试。
* `validate_script`: 快速验证(基本/标准)以在写入前后捕获语法/结构问题。 * `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 和基本元数据,而不返回文件内容。
</details>
<details open>
<summary><strong> 可用资源 </strong></summary>
您的大语言模型可以检索以下资源:
* `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 中定义的所有标签。
</details> </details>
--- ---

View File

@ -40,19 +40,23 @@ MCP for Unity acts as a bridge, allowing AI assistants (like Claude, Cursor) to
Your LLM can use functions like: Your LLM can use functions like:
* `read_console`: Gets messages from or clears the console. * `execute_menu_item`: Executes Unity Editor menu items (e.g., "File/Save Project").
* `manage_script`: Manages C# scripts (create, read, update, delete). * `manage_asset`: Performs asset operations (import, create, modify, delete, etc.).
* `manage_editor`: Controls and queries the editor's state and settings. * `manage_editor`: Controls and queries the editor's state and settings.
* `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.). * `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations.
* `manage_asset`: Performs asset operations (import, create, modify, delete, etc.). * `manage_prefabs`: Performs prefab operations (create, modify, delete, etc.).
* `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). * `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.).
* `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations. * `manage_script`: Compatibility router for legacy script operations (create, read, delete). Prefer `apply_text_edits` or `script_apply_edits` for edits.
* `execute_menu_item`: Executes Unity Editor menu items (e.g., "File/Save Project"). * `manage_shader`: Performs shader CRUD operations (create, read, modify, delete).
* `apply_text_edits`: Precise text edits with precondition hashes and atomic multi-edit batches. * `read_console`: Gets messages from or clears the console.
* `script_apply_edits`: Structured C# method/class edits (insert/replace/delete) with safer boundaries. * `run_tests`: Runs tests in the Unity Editor.
* `validate_script`: Fast validation (basic/standard) to catch syntax/structure issues before/after writes. * `set_active_instance`: Routes subsequent tool calls to a specific Unity instance (when multiple are running).
* `run_test`: Runs a tests in the Unity Editor. * `apply_text_edits`: Precise text edits with precondition hashes and atomic multi-edit batches.
* `set_active_instance`: Routes subsequent tool calls to a specific Unity instance (when multiple are running). * `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.
</details> </details>
@ -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: Your LLM can retrieve the following resources:
* `unity_instances`: Lists all running Unity Editor instances with their details (name, path, port, status). * `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. * `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"). * `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.
</details> </details>
--- ---

View File

@ -13,7 +13,7 @@ from unity_connection import get_unity_connection_pool
name="unity_instances", name="unity_instances",
description="Lists all running Unity Editor instances with their details." 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. List all available Unity Editor instances.
@ -30,7 +30,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]:
Returns: Returns:
Dictionary containing list of instances and metadata Dictionary containing list of instances and metadata
""" """
ctx.info("Listing Unity instances") await ctx.info("Listing Unity instances")
try: try:
pool = get_unity_connection_pool() pool = get_unity_connection_pool()
@ -58,7 +58,7 @@ def unity_instances(ctx: Context) -> dict[str, Any]:
return result return result
except Exception as e: except Exception as e:
ctx.error(f"Error listing Unity instances: {e}") await ctx.error(f"Error listing Unity instances: {e}")
return { return {
"success": False, "success": False,
"error": f"Failed to list Unity instances: {str(e)}", "error": f"Failed to list Unity instances: {str(e)}",

View File

@ -108,12 +108,14 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
instances = _unity_connection_pool.discover_all_instances() instances = _unity_connection_pool.discover_all_instances()
if 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 to connect to default instance
try: try:
_unity_connection_pool.get_connection() _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) # Record successful Unity connection (deferred)
import threading as _t import threading as _t
@ -126,7 +128,8 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
} }
)).start() )).start()
except Exception as e: 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: else:
logger.warning("No Unity instances found on startup") logger.warning("No Unity instances found on startup")
@ -172,23 +175,31 @@ mcp = FastMCP(
name="mcp-for-unity-server", name="mcp-for-unity-server",
lifespan=server_lifespan, lifespan=server_lifespan,
instructions=""" instructions="""
This server provides tools to interact with the Unity Game Engine Editor.\n\n This server provides tools to interact with the Unity Game Engine Editor.
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
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 @@ Examples:
# Set environment variable if --default-instance is provided # Set environment variable if --default-instance is provided
if args.default_instance: if args.default_instance:
os.environ["UNITY_MCP_DEFAULT_INSTANCE"] = 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') mcp.run(transport='stdio')

View File

@ -44,11 +44,11 @@ class RunTestsResponse(MCPResponse):
) )
async def run_tests( async def run_tests(
ctx: Context, ctx: Context,
mode: Annotated[Literal["edit", "play"], "Unity test mode to run"] = "edit", mode: Annotated[Literal["EditMode", "PlayMode"], Field(
timeout_seconds: Annotated[int | str | None, "Optional timeout in seconds for the Unity test run (string, e.g. '30')"] = None, description="Unity test mode to run")] = "EditMode",
) -> dict[str, Any]: timeout_seconds: Annotated[int | str, Field(
# Get active instance from session state description="Optional timeout in seconds for the Unity test run (string, e.g. '30')")] | None = None,
# Removed session_state import ) -> RunTestsResponse:
unity_instance = get_unity_instance_from_context(ctx) unity_instance = get_unity_instance_from_context(ctx)
# Coerce timeout defensively (string/float -> int) # Coerce timeout defensively (string/float -> int)

View File

@ -1,7 +1,6 @@
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using System.Collections;
using UnityEditor; using UnityEditor;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Tools;

View File

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Tools;
using static MCPForUnity.Editor.Tools.ManageGameObject;
namespace MCPForUnityTests.Editor.Tools namespace MCPForUnityTests.Editor.Tools
{ {

View File

@ -1,8 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Tools;

View File

@ -2,7 +2,6 @@ using System;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Tools;
using static MCPForUnity.Editor.Tools.ManageGameObject;
namespace MCPForUnityTests.Editor.Tools namespace MCPForUnityTests.Editor.Tools
{ {

View File

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEditor;
using UnityEngine.TestTools; using UnityEngine.TestTools;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Tools;

View File

@ -5,7 +5,6 @@ using UnityEditor;
using UnityEditor.SceneManagement; using UnityEditor.SceneManagement;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Tools.Prefabs; using MCPForUnity.Editor.Tools.Prefabs;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools namespace MCPForUnityTests.Editor.Tools
{ {
@ -53,11 +52,11 @@ namespace MCPForUnityTests.Editor.Tools
Assert.IsTrue(openResult.Value<bool>("success"), "open_stage should succeed for a valid prefab."); Assert.IsTrue(openResult.Value<bool>("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.IsNotNull(stage, "Prefab stage should be open after open_stage.");
Assert.AreEqual(prefabPath, stage.assetPath, "Opened stage should match prefab path."); 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<bool>("success"), "get_prefab_stage should succeed when stage is open."); Assert.IsTrue(stageInfo.Value<bool>("success"), "get_prefab_stage should succeed when stage is open.");
var data = stageInfo["data"] as JObject; var data = stageInfo["data"] as JObject;
@ -125,7 +124,7 @@ namespace MCPForUnityTests.Editor.Tools
["prefabPath"] = prefabPath ["prefabPath"] = prefabPath
}); });
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); UnityEditor.SceneManagement.PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
Assert.IsNotNull(stage, "Stage should be open before modifying."); Assert.IsNotNull(stage, "Stage should be open before modifying.");
stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f); stage.prefabContentsRoot.transform.localScale = new Vector3(2f, 2f, 2f);

View File

@ -1,10 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
using UnityEditor;
using UnityEngine.TestTools;
using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Helpers;
namespace MCPForUnityTests.Editor.Tools namespace MCPForUnityTests.Editor.Tools

View File

@ -29,7 +29,7 @@ Create a Python file **anywhere in your Unity project**. For example, `Assets/Ed
```python ```python
from typing import Annotated, Any from typing import Annotated, Any
from mcp.server.fastmcp import Context from fastmcp import Context
from registry import mcp_for_unity_tool from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry 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 ```python
from typing import Annotated, Any from typing import Annotated, Any
from mcp.server.fastmcp import Context from fastmcp import Context
from registry import mcp_for_unity_tool from registry import mcp_for_unity_tool
from unity_connection import send_command_with_retry from unity_connection import send_command_with_retry

View File

@ -9,32 +9,44 @@ Welcome to the MCP for Unity development environment! This directory contains to
### Installing Development Dependencies ### 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 ```bash
# Navigate to the server source directory # Navigate to the server source directory
cd MCPForUnity/UnityMcpServer~/src cd MCPForUnity/UnityMcpServer~/src
# Install the package in editable mode with dev dependencies # Install the package in editable mode with dev dependencies
pip install -e .[dev] uv pip install -e ".[dev]"
``` ```
This installs: This installs:
- **Runtime dependencies**: `httpx`, `mcp`, `pydantic`, `tomli` - **Runtime dependencies**: `httpx`, `fastmcp`, `mcp`, `pydantic`, `tomli`
- **Development dependencies**: `pytest`, `pytest-anyio` - **Development dependencies**: `pytest`, `pytest-asyncio`
### Running Tests ### Running Tests
```bash ```bash
# From the repo root # From the server source directory
pytest tests/ -v cd MCPForUnity/UnityMcpServer~/src
uv run pytest tests/ -v
``` ```
Or if you prefer using Python module syntax: Or from the repo root:
```bash ```bash
python -m pytest tests/ -v # 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
uv run pytest tests/ -v -m unit
``` ```
## 🚀 Available Development Features ## 🚀 Available Development Features

View File

@ -1,4 +0,0 @@
[pytest]
testpaths = tests
norecursedirs = UnityMcpBridge MCPForUnity

View File

@ -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")
)

View File

@ -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

View File

@ -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)