Payload-safe paging for hierarchy/components + safer asset search + docs (#490)

* Fix test teardown to avoid dropping MCP bridge

CodexConfigHelperTests was calling MCPServiceLocator.Reset() in TearDown, which disposes the active bridge/transport during MCP-driven test runs. Replace with restoring only the mutated service (IPlatformService).

* Avoid leaking PlatformService in CodexConfigHelperTests

Capture the original IPlatformService before this fixture runs and restore it in TearDown. This preserves the MCP connection safety fix (no MCPServiceLocator.Reset()) while avoiding global state leakage to subsequent tests.

* Fix SO MCP tooling: validate folder roots, normalize paths, expand tests; remove vestigial SO tools

* Remove UnityMCPTests stress artifacts and ignore Assets/Temp

* Ignore UnityMCPTests Assets/Temp only

* Clarify array_resize fallback logic comments

* Refactor: simplify action set and reuse slash sanitization

* Enhance: preserve GUID on overwrite & support Vector/Color types in ScriptableObject tools

* Fix: ensure asset name matches filename to suppress Unity warnings

* Fix: resolve Unity warnings by ensuring asset name match and removing redundant import

* Refactor: Validate assetName, strict object parsing for vectors, remove broken SO logic from ManageAsset

* Hardening: reject Windows drive paths; clarify supported asset types

* Delete FixscriptableobjecPlan.md

* Paginate get_hierarchy and get_components to prevent large payload crashes

* dev: add uvx dev-mode refresh + safer HTTP stop; fix server typing eval

* Payload-safe paging defaults + docs; harden asset search; stabilize Codex tests

* chore: align uvx args + coercion helpers; tighten safety guidance

* chore: minor cleanup + stabilize EditMode SO tests
main
dsarno 2025-12-28 20:57:57 -08:00 committed by GitHub
parent 28f60b42b0
commit 35165e11b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1459 additions and 197 deletions

View File

@ -423,7 +423,9 @@ namespace MCPForUnity.Editor.Clients
else
{
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" {packageName}";
bool devForceRefresh = GetDevModeForceRefresh();
string devFlags = devForceRefresh ? "--no-cache --refresh " : string.Empty;
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
}
string projectDir = Path.GetDirectoryName(Application.dataPath);
@ -537,14 +539,23 @@ namespace MCPForUnity.Editor.Clients
}
string gitUrl = AssetPathUtility.GetMcpServerGitUrl();
bool devForceRefresh = GetDevModeForceRefresh();
string devFlags = devForceRefresh ? "--no-cache --refresh " : string.Empty;
return "# Register the MCP server with Claude Code:\n" +
$"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" mcp-for-unity\n\n" +
$"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" mcp-for-unity\n\n" +
"# Unregister the MCP server:\n" +
"claude mcp remove UnityMCP\n\n" +
"# List registered servers:\n" +
"claude mcp list # Only works when claude is run in the project's directory";
}
private static bool GetDevModeForceRefresh()
{
try { return EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); }
catch { return false; }
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed",

View File

@ -20,6 +20,7 @@ namespace MCPForUnity.Editor.Constants
internal const string SessionId = "MCPForUnity.SessionId";
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh";
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";
internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath";

View File

@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
using MCPForUnity.External.Tommy;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
@ -15,6 +17,26 @@ namespace MCPForUnity.Editor.Helpers
/// </summary>
public static class CodexConfigHelper
{
private static bool GetDevModeForceRefresh()
{
try
{
return EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
}
catch
{
return false;
}
}
private static void AddDevModeArgs(TomlArray args, bool devForceRefresh)
{
if (args == null) return;
if (!devForceRefresh) return;
args.Add(new TomlString { Value = "--no-cache" });
args.Add(new TomlString { Value = "--refresh" });
}
public static string BuildCodexServerBlock(string uvPath)
{
var table = new TomlTable();
@ -37,9 +59,12 @@ namespace MCPForUnity.Editor.Helpers
{
// Stdio mode: Use command and args
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool devForceRefresh = GetDevModeForceRefresh();
unityMCP["command"] = uvxPath;
var args = new TomlArray();
AddDevModeArgs(args, devForceRefresh);
if (!string.IsNullOrEmpty(fromUrl))
{
args.Add(new TomlString { Value = "--from" });
@ -184,9 +209,12 @@ namespace MCPForUnity.Editor.Helpers
{
// Stdio mode: Use command and args
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool devForceRefresh = GetDevModeForceRefresh();
unityMCP["command"] = new TomlString { Value = uvxPath };
var argsArray = new TomlArray();
AddDevModeArgs(argsArray, devForceRefresh);
if (!string.IsNullOrEmpty(fromUrl))
{
argsArray.Add(new TomlString { Value = "--from" });

View File

@ -81,7 +81,10 @@ namespace MCPForUnity.Editor.Helpers
// Stdio mode: Use uvx command
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
var toolArgs = BuildUvxArgs(fromUrl, packageName);
bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }
var toolArgs = BuildUvxArgs(fromUrl, packageName, devForceRefresh);
if (ShouldUseWindowsCmdShim(client))
{
@ -149,15 +152,23 @@ namespace MCPForUnity.Editor.Helpers
return created;
}
private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
private static IList<string> BuildUvxArgs(string fromUrl, string packageName, bool devForceRefresh)
{
var args = new List<string> { packageName };
// Dev mode: force a fresh install/resolution (avoids stale cached builds while iterating).
// `--no-cache` is the key flag; `--refresh` ensures metadata is revalidated.
// Keep ordering consistent with other uvx builders: dev flags first, then --from <url>, then package name.
var args = new List<string>();
if (devForceRefresh)
{
args.Add("--no-cache");
args.Add("--refresh");
}
if (!string.IsNullOrEmpty(fromUrl))
{
args.Insert(0, fromUrl);
args.Insert(0, "--from");
args.Add("--from");
args.Add(fromUrl);
}
args.Add(packageName);
args.Add("--transport");
args.Add("stdio");

View File

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
@ -171,15 +172,7 @@ namespace MCPForUnity.Editor.Services
// First, try to stop any existing server
StopLocalHttpServer();
// Clear the cache to ensure we get a fresh version
try
{
ClearUvxCache();
}
catch (Exception ex)
{
McpLog.Warn($"Failed to clear cache before starting server: {ex.Message}");
}
// Note: Dev mode cache-busting is handled by `uvx --no-cache --refresh` in the generated command.
if (EditorUtility.DisplayDialog(
"Start Local HTTP Server",
@ -237,20 +230,47 @@ namespace MCPForUnity.Editor.Services
return false;
}
McpLog.Info($"Attempting to stop any process listening on local port {port}. This will terminate the owning process even if it is not the MCP server.");
// Guardrails:
// - Never terminate the Unity Editor process.
// - Only terminate processes that look like the MCP server (uv/uvx/python running mcp-for-unity).
// This prevents accidental termination of unrelated services (including Unity itself).
int unityPid = GetCurrentProcessIdSafe();
int pid = GetProcessIdForPort(port);
if (pid > 0)
{
KillProcess(pid);
McpLog.Info($"Stopped local HTTP server on port {port} (PID: {pid})");
return true;
}
else
var pids = GetListeningProcessIdsForPort(port);
if (pids.Count == 0)
{
McpLog.Info($"No process found listening on port {port}");
return false;
}
bool stoppedAny = false;
foreach (var pid in pids)
{
if (pid <= 0) continue;
if (unityPid > 0 && pid == unityPid)
{
McpLog.Warn($"Refusing to stop port {port}: owning PID appears to be the Unity Editor process (PID {pid}).");
continue;
}
if (!LooksLikeMcpServerProcess(pid))
{
McpLog.Warn($"Refusing to stop port {port}: owning PID {pid} does not look like mcp-for-unity (uvx/uv/python).");
continue;
}
if (TerminateProcess(pid))
{
McpLog.Info($"Stopped local HTTP server on port {port} (PID: {pid})");
stoppedAny = true;
}
else
{
McpLog.Warn($"Failed to stop process PID {pid} on port {port}");
}
}
return stoppedAny;
}
catch (Exception ex)
{
@ -259,8 +279,9 @@ namespace MCPForUnity.Editor.Services
}
}
private int GetProcessIdForPort(int port)
private List<int> GetListeningProcessIdsForPort(int port)
{
var results = new List<int>();
try
{
string stdout, stderr;
@ -280,7 +301,7 @@ namespace MCPForUnity.Editor.Services
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 0 && int.TryParse(parts[parts.Length - 1], out int pid))
{
return pid;
results.Add(pid);
}
}
}
@ -288,12 +309,13 @@ namespace MCPForUnity.Editor.Services
}
else
{
// lsof -i :<port> -t
// lsof: only return LISTENers (avoids capturing random clients)
// Use /usr/sbin/lsof directly as it might not be in PATH for Unity
string lsofPath = "/usr/sbin/lsof";
if (!System.IO.File.Exists(lsofPath)) lsofPath = "lsof"; // Fallback
success = ExecPath.TryRun(lsofPath, $"-i :{port} -t", Application.dataPath, out stdout, out stderr);
// -nP: avoid DNS/service name lookups; faster and less error-prone
success = ExecPath.TryRun(lsofPath, $"-nP -iTCP:{port} -sTCP:LISTEN -t", Application.dataPath, out stdout, out stderr);
if (success && !string.IsNullOrWhiteSpace(stdout))
{
var pidStrings = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
@ -301,12 +323,7 @@ namespace MCPForUnity.Editor.Services
{
if (int.TryParse(pidString.Trim(), out int pid))
{
if (pidStrings.Length > 1)
{
McpLog.Debug($"Multiple processes found on port {port}; attempting to stop PID {pid} returned by lsof -t.");
}
return pid;
results.Add(pid);
}
}
}
@ -316,26 +333,96 @@ namespace MCPForUnity.Editor.Services
{
McpLog.Warn($"Error checking port {port}: {ex.Message}");
}
return -1;
return results.Distinct().ToList();
}
private void KillProcess(int pid)
private static int GetCurrentProcessIdSafe()
{
try { return System.Diagnostics.Process.GetCurrentProcess().Id; }
catch { return -1; }
}
private bool LooksLikeMcpServerProcess(int pid)
{
try
{
// Windows best-effort: tasklist /FI "PID eq X"
if (Application.platform == RuntimePlatform.WindowsEditor)
{
if (ExecPath.TryRun("cmd.exe", $"/c tasklist /FI \"PID eq {pid}\"", Application.dataPath, out var stdout, out var stderr, 5000))
{
string combined = (stdout ?? string.Empty) + "\n" + (stderr ?? string.Empty);
combined = combined.ToLowerInvariant();
// Common process names: python.exe, uv.exe, uvx.exe
return combined.Contains("python") || combined.Contains("uvx") || combined.Contains("uv.exe") || combined.Contains("uvx.exe");
}
return false;
}
// macOS/Linux: ps -p pid -o comm= -o args=
if (ExecPath.TryRun("ps", $"-p {pid} -o comm= -o args=", Application.dataPath, out var psOut, out var psErr, 5000))
{
string s = (psOut ?? string.Empty).Trim().ToLowerInvariant();
if (string.IsNullOrEmpty(s))
{
s = (psErr ?? string.Empty).Trim().ToLowerInvariant();
}
// Explicitly never kill Unity / Unity Hub processes
if (s.Contains("unity") || s.Contains("unityhub") || s.Contains("unity hub"))
{
return false;
}
// Positive indicators
bool mentionsUvx = s.Contains("uvx") || s.Contains(" uvx ");
bool mentionsUv = s.Contains("uv ") || s.Contains("/uv");
bool mentionsPython = s.Contains("python");
bool mentionsMcp = s.Contains("mcp-for-unity") || s.Contains("mcp_for_unity") || s.Contains("mcp for unity");
bool mentionsTransport = s.Contains("--transport") && s.Contains("http");
// Accept if it looks like uv/uvx/python launching our server package/entrypoint
if ((mentionsUvx || mentionsUv || mentionsPython) && (mentionsMcp || mentionsTransport))
{
return true;
}
}
}
catch { }
return false;
}
private bool TerminateProcess(int pid)
{
try
{
string stdout, stderr;
if (Application.platform == RuntimePlatform.WindowsEditor)
{
ExecPath.TryRun("taskkill", $"/F /PID {pid}", Application.dataPath, out stdout, out stderr);
// taskkill without /F first; fall back to /F if needed.
bool ok = ExecPath.TryRun("taskkill", $"/PID {pid}", Application.dataPath, out stdout, out stderr);
if (!ok)
{
ok = ExecPath.TryRun("taskkill", $"/F /PID {pid}", Application.dataPath, out stdout, out stderr);
}
return ok;
}
else
{
ExecPath.TryRun("kill", $"-9 {pid}", Application.dataPath, out stdout, out stderr);
// Try a graceful termination first, then escalate.
bool ok = ExecPath.TryRun("kill", $"-15 {pid}", Application.dataPath, out stdout, out stderr);
if (!ok)
{
ok = ExecPath.TryRun("kill", $"-9 {pid}", Application.dataPath, out stdout, out stderr);
}
return ok;
}
}
catch (Exception ex)
{
McpLog.Error($"Error killing process {pid}: {ex.Message}");
return false;
}
}
@ -368,9 +455,13 @@ namespace MCPForUnity.Editor.Services
return false;
}
bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }
string devFlags = devForceRefresh ? "--no-cache --refresh " : string.Empty;
string args = string.IsNullOrEmpty(fromUrl)
? $"{packageName} --transport http --http-url {httpUrl}"
: $"--from {fromUrl} {packageName} --transport http --http-url {httpUrl}";
? $"{devFlags}{packageName} --transport http --http-url {httpUrl}"
: $"{devFlags}--from {fromUrl} {packageName} --transport http --http-url {httpUrl}";
command = $"{uvxPath} {args}";
return true;

View File

@ -180,8 +180,44 @@ namespace MCPForUnity.Editor.Tools
return new ErrorResponse(
"'target' parameter required for get_components."
);
// Pass the includeNonPublicSerialized flag here
return GetComponentsFromTarget(getCompTarget, searchMethod, includeNonPublicSerialized);
// Paging + safety: return metadata by default; deep fields are opt-in.
int CoerceInt(JToken t, int @default)
{
if (t == null || t.Type == JTokenType.Null) return @default;
try
{
if (t.Type == JTokenType.Integer) return t.Value<int>();
var s = t.ToString().Trim();
if (s.Length == 0) return @default;
if (int.TryParse(s, out var i)) return i;
if (double.TryParse(s, out var d)) return (int)d;
}
catch { }
return @default;
}
bool CoerceBool(JToken t, bool @default)
{
if (t == null || t.Type == JTokenType.Null) return @default;
try
{
if (t.Type == JTokenType.Boolean) return t.Value<bool>();
var s = t.ToString().Trim();
if (s.Length == 0) return @default;
if (bool.TryParse(s, out var b)) return b;
if (s == "1") return true;
if (s == "0") return false;
}
catch { }
return @default;
}
int pageSize = CoerceInt(@params["pageSize"] ?? @params["page_size"], 25);
int cursor = CoerceInt(@params["cursor"], 0);
int maxComponents = CoerceInt(@params["maxComponents"] ?? @params["max_components"], 50);
bool includeProperties = CoerceBool(@params["includeProperties"] ?? @params["include_properties"], false);
// Pass the includeNonPublicSerialized flag through, but only used if includeProperties is true.
return GetComponentsFromTarget(getCompTarget, searchMethod, includeNonPublicSerialized, pageSize, cursor, maxComponents, includeProperties);
case "get_component":
string getSingleCompTarget = targetToken?.ToString();
if (getSingleCompTarget == null)
@ -1191,7 +1227,15 @@ namespace MCPForUnity.Editor.Tools
return new SuccessResponse($"Found {results.Count} GameObject(s).", results);
}
private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized = true)
private static object GetComponentsFromTarget(
string target,
string searchMethod,
bool includeNonPublicSerialized = true,
int pageSize = 25,
int cursor = 0,
int maxComponents = 50,
bool includeProperties = false
)
{
GameObject targetGo = FindObjectInternal(target, searchMethod);
if (targetGo == null)
@ -1203,57 +1247,90 @@ namespace MCPForUnity.Editor.Tools
try
{
// --- Get components, immediately copy to list, and null original array ---
Component[] originalComponents = targetGo.GetComponents<Component>();
List<Component> componentsToIterate = new List<Component>(originalComponents ?? Array.Empty<Component>()); // Copy immediately, handle null case
int componentCount = componentsToIterate.Count;
originalComponents = null; // Null the original reference
// Debug.Log($"[GetComponentsFromTarget] Found {componentCount} components on {targetGo.name}. Copied to list, nulled original. Starting REVERSE for loop...");
// --- End Copy and Null ---
int resolvedPageSize = Mathf.Clamp(pageSize, 1, 200);
int resolvedCursor = Mathf.Max(0, cursor);
int resolvedMaxComponents = Mathf.Clamp(maxComponents, 1, 500);
int effectiveTake = Mathf.Min(resolvedPageSize, resolvedMaxComponents);
var componentData = new List<object>();
for (int i = componentCount - 1; i >= 0; i--) // Iterate backwards over the COPY
// Build a stable list once; pagination is applied to this list.
var all = targetGo.GetComponents<Component>();
var components = new List<Component>(all?.Length ?? 0);
if (all != null)
{
Component c = componentsToIterate[i]; // Use the copy
if (c == null)
for (int i = 0; i < all.Length; i++)
{
// Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] Encountered a null component at index {i} on {targetGo.name}. Skipping.");
continue; // Safety check
if (all[i] != null) components.Add(all[i]);
}
// Debug.Log($"[GetComponentsFromTarget REVERSE for] Processing component: {c.GetType()?.FullName ?? "null"} (ID: {c.GetInstanceID()}) at index {i} on {targetGo.name}");
}
int total = components.Count;
if (resolvedCursor > total) resolvedCursor = total;
int end = Mathf.Min(total, resolvedCursor + effectiveTake);
var items = new List<object>(Mathf.Max(0, end - resolvedCursor));
// If caller explicitly asked for properties, we still enforce a conservative payload budget.
const int maxPayloadChars = 250_000; // ~250KB assuming 1 char ~= 1 byte ASCII-ish
int payloadChars = 0;
for (int i = resolvedCursor; i < end; i++)
{
var c = components[i];
if (c == null) continue;
if (!includeProperties)
{
items.Add(BuildComponentMetadata(c));
continue;
}
try
{
var data = Helpers.GameObjectSerializer.GetComponentData(c, includeNonPublicSerialized);
if (data != null) // Ensure GetComponentData didn't return null
if (data == null) continue;
// Rough cap to keep responses from exploding even when includeProperties is true.
var token = JToken.FromObject(data);
int addChars = token.ToString(Newtonsoft.Json.Formatting.None).Length;
if (payloadChars + addChars > maxPayloadChars && items.Count > 0)
{
componentData.Insert(0, data); // Insert at beginning to maintain original order in final list
// Stop early; next_cursor will allow fetching more (or caller can use get_component).
end = i;
break;
}
// else
// {
// Debug.LogWarning($"[GetComponentsFromTarget REVERSE for] GetComponentData returned null for component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}. Skipping addition.");
// }
payloadChars += addChars;
items.Add(token);
}
catch (Exception ex)
{
Debug.LogError($"[GetComponentsFromTarget REVERSE for] Error processing component {c.GetType().FullName} (ID: {c.GetInstanceID()}) on {targetGo.name}: {ex.Message}\n{ex.StackTrace}");
// Optionally add placeholder data or just skip
componentData.Insert(0, new JObject( // Insert error marker at beginning
new JProperty("typeName", c.GetType().FullName + " (Serialization Error)"),
new JProperty("instanceID", c.GetInstanceID()),
new JProperty("error", ex.Message)
));
// Avoid throwing; mark the component as failed.
items.Add(
new JObject(
new JProperty("typeName", c.GetType().FullName + " (Serialization Error)"),
new JProperty("instanceID", c.GetInstanceID()),
new JProperty("error", ex.Message)
)
);
}
}
// Debug.Log($"[GetComponentsFromTarget] Finished REVERSE for loop.");
// Cleanup the list we created
componentsToIterate.Clear();
componentsToIterate = null;
bool truncated = end < total;
string nextCursor = truncated ? end.ToString() : null;
var payload = new
{
cursor = resolvedCursor,
pageSize = effectiveTake,
next_cursor = nextCursor,
truncated = truncated,
total = total,
includeProperties = includeProperties,
items = items,
};
return new SuccessResponse(
$"Retrieved {componentData.Count} components from '{targetGo.name}'.",
componentData // List was built in original order
$"Retrieved components page from '{targetGo.name}'.",
payload
);
}
catch (Exception e)
@ -1264,6 +1341,21 @@ namespace MCPForUnity.Editor.Tools
}
}
private static object BuildComponentMetadata(Component c)
{
if (c == null) return null;
var d = new Dictionary<string, object>
{
{ "typeName", c.GetType().FullName },
{ "instanceID", c.GetInstanceID() },
};
if (c is Behaviour b)
{
d["enabled"] = b.enabled;
}
return d;
}
private static object GetSingleComponentFromTarget(string target, string searchMethod, string componentName, bool includeNonPublicSerialized = true)
{
GameObject targetGo = FindObjectInternal(target, searchMethod);

View File

@ -26,6 +26,15 @@ namespace MCPForUnity.Editor.Tools
public int? buildIndex { get; set; }
public string fileName { get; set; } = string.Empty;
public int? superSize { get; set; }
// get_hierarchy paging + safety (summary-first)
public JToken parent { get; set; }
public int? pageSize { get; set; }
public int? cursor { get; set; }
public int? maxNodes { get; set; }
public int? maxDepth { get; set; }
public int? maxChildrenPerNode { get; set; }
public bool? includeTransform { get; set; }
}
private static SceneCommand ToSceneCommand(JObject p)
@ -40,6 +49,21 @@ namespace MCPForUnity.Editor.Tools
if (double.TryParse(s, out var d)) return (int)d;
return t.Type == JTokenType.Integer ? t.Value<int>() : (int?)null;
}
bool? BB(JToken t)
{
if (t == null || t.Type == JTokenType.Null) return null;
try
{
if (t.Type == JTokenType.Boolean) return t.Value<bool>();
var s = t.ToString().Trim();
if (s.Length == 0) return null;
if (bool.TryParse(s, out var b)) return b;
if (s == "1") return true;
if (s == "0") return false;
}
catch { }
return null;
}
return new SceneCommand
{
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
@ -47,7 +71,16 @@ namespace MCPForUnity.Editor.Tools
path = p["path"]?.ToString() ?? string.Empty,
buildIndex = BI(p["buildIndex"] ?? p["build_index"]),
fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty,
superSize = BI(p["superSize"] ?? p["super_size"] ?? p["supersize"])
superSize = BI(p["superSize"] ?? p["super_size"] ?? p["supersize"]),
// get_hierarchy paging + safety
parent = p["parent"],
pageSize = BI(p["pageSize"] ?? p["page_size"]),
cursor = BI(p["cursor"]),
maxNodes = BI(p["maxNodes"] ?? p["max_nodes"]),
maxDepth = BI(p["maxDepth"] ?? p["max_depth"]),
maxChildrenPerNode = BI(p["maxChildrenPerNode"] ?? p["max_children_per_node"]),
includeTransform = BB(p["includeTransform"] ?? p["include_transform"]),
};
}
@ -137,7 +170,7 @@ namespace MCPForUnity.Editor.Tools
return SaveScene(fullPath, relativePath);
case "get_hierarchy":
try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { }
var gh = GetSceneHierarchy();
var gh = GetSceneHierarchyPaged(cmd);
try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { }
return gh;
case "get_active":
@ -452,7 +485,7 @@ namespace MCPForUnity.Editor.Tools
}
}
private static object GetSceneHierarchy()
private static object GetSceneHierarchyPaged(SceneCommand cmd)
{
try
{
@ -466,15 +499,71 @@ namespace MCPForUnity.Editor.Tools
);
}
try { McpLog.Info("[ManageScene] get_hierarchy: fetching root objects", always: false); } catch { }
GameObject[] rootObjects = activeScene.GetRootGameObjects();
try { McpLog.Info($"[ManageScene] get_hierarchy: rootCount={rootObjects?.Length ?? 0}", always: false); } catch { }
var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
// Defaults tuned for safety; callers can override but we clamp to sane maxes.
// NOTE: pageSize is "items per page", not "number of pages".
// Keep this conservative to reduce peak response sizes when callers omit page_size.
int resolvedPageSize = Mathf.Clamp(cmd.pageSize ?? 50, 1, 500);
int resolvedCursor = Mathf.Max(0, cmd.cursor ?? 0);
int resolvedMaxNodes = Mathf.Clamp(cmd.maxNodes ?? 1000, 1, 5000);
int effectiveTake = Mathf.Min(resolvedPageSize, resolvedMaxNodes);
int resolvedMaxChildrenPerNode = Mathf.Clamp(cmd.maxChildrenPerNode ?? 200, 0, 2000);
bool includeTransform = cmd.includeTransform ?? false;
var resp = new SuccessResponse(
$"Retrieved hierarchy for scene '{activeScene.name}'.",
hierarchy
);
// NOTE: maxDepth is accepted for forward-compatibility, but current paging mode
// returns a single level (roots or direct children). This keeps payloads bounded.
List<GameObject> nodes;
string scope;
GameObject parentGo = ResolveGameObject(cmd.parent, activeScene);
if (cmd.parent == null || cmd.parent.Type == JTokenType.Null)
{
try { McpLog.Info("[ManageScene] get_hierarchy: listing root objects (paged summary)", always: false); } catch { }
nodes = activeScene.GetRootGameObjects().Where(go => go != null).ToList();
scope = "roots";
}
else
{
if (parentGo == null)
{
return new ErrorResponse($"Parent GameObject ('{cmd.parent}') not found.");
}
try { McpLog.Info($"[ManageScene] get_hierarchy: listing children of '{parentGo.name}' (paged summary)", always: false); } catch { }
nodes = new List<GameObject>(parentGo.transform.childCount);
foreach (Transform child in parentGo.transform)
{
if (child != null) nodes.Add(child.gameObject);
}
scope = "children";
}
int total = nodes.Count;
if (resolvedCursor > total) resolvedCursor = total;
int end = Mathf.Min(total, resolvedCursor + effectiveTake);
var items = new List<object>(Mathf.Max(0, end - resolvedCursor));
for (int i = resolvedCursor; i < end; i++)
{
var go = nodes[i];
if (go == null) continue;
items.Add(BuildGameObjectSummary(go, includeTransform, resolvedMaxChildrenPerNode));
}
bool truncated = end < total;
string nextCursor = truncated ? end.ToString() : null;
var payload = new
{
scope = scope,
cursor = resolvedCursor,
pageSize = effectiveTake,
next_cursor = nextCursor,
truncated = truncated,
total = total,
items = items,
};
var resp = new SuccessResponse($"Retrieved hierarchy page for scene '{activeScene.name}'.", payload);
try { McpLog.Info("[ManageScene] get_hierarchy: success", always: false); } catch { }
return resp;
}
@ -485,6 +574,111 @@ namespace MCPForUnity.Editor.Tools
}
}
private static GameObject ResolveGameObject(JToken targetToken, Scene activeScene)
{
if (targetToken == null || targetToken.Type == JTokenType.Null) return null;
try
{
if (targetToken.Type == JTokenType.Integer || int.TryParse(targetToken.ToString(), out _))
{
if (int.TryParse(targetToken.ToString(), out int id))
{
var obj = EditorUtility.InstanceIDToObject(id);
if (obj is GameObject go) return go;
if (obj is Component c) return c.gameObject;
}
}
}
catch { }
string s = targetToken.ToString();
if (string.IsNullOrEmpty(s)) return null;
// Path-based find (e.g., "Root/Child/GrandChild")
if (s.Contains("/"))
{
try { return GameObject.Find(s); } catch { }
}
// Name-based find (first match, includes inactive)
try
{
var all = activeScene.GetRootGameObjects();
foreach (var root in all)
{
if (root == null) continue;
if (root.name == s) return root;
var trs = root.GetComponentsInChildren<Transform>(includeInactive: true);
foreach (var t in trs)
{
if (t != null && t.gameObject != null && t.gameObject.name == s) return t.gameObject;
}
}
}
catch { }
return null;
}
private static object BuildGameObjectSummary(GameObject go, bool includeTransform, int maxChildrenPerNode)
{
if (go == null) return null;
int childCount = 0;
try { childCount = go.transform != null ? go.transform.childCount : 0; } catch { }
bool childrenTruncated = childCount > 0; // We do not inline children in summary mode.
var d = new Dictionary<string, object>
{
{ "name", go.name },
{ "instanceID", go.GetInstanceID() },
{ "activeSelf", go.activeSelf },
{ "activeInHierarchy", go.activeInHierarchy },
{ "tag", go.tag },
{ "layer", go.layer },
{ "isStatic", go.isStatic },
{ "path", GetGameObjectPath(go) },
{ "childCount", childCount },
{ "childrenTruncated", childrenTruncated },
{ "childrenCursor", childCount > 0 ? "0" : null },
{ "childrenPageSizeDefault", maxChildrenPerNode },
};
if (includeTransform && go.transform != null)
{
var t = go.transform;
d["transform"] = new
{
position = new[] { t.localPosition.x, t.localPosition.y, t.localPosition.z },
rotation = new[] { t.localRotation.eulerAngles.x, t.localRotation.eulerAngles.y, t.localRotation.eulerAngles.z },
scale = new[] { t.localScale.x, t.localScale.y, t.localScale.z },
};
}
return d;
}
private static string GetGameObjectPath(GameObject go)
{
if (go == null) return string.Empty;
try
{
var names = new Stack<string>();
Transform t = go.transform;
while (t != null)
{
names.Push(t.name);
t = t.parent;
}
return string.Join("/", names);
}
catch
{
return go.name;
}
}
/// <summary>
/// Recursively builds a data representation of a GameObject and its children.
/// </summary>

View File

@ -30,6 +30,7 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
private TextField gitUrlOverride;
private Button browseGitUrlButton;
private Button clearGitUrlButton;
private Toggle devModeForceRefreshToggle;
private TextField deploySourcePath;
private Button browseDeploySourceButton;
private Button clearDeploySourceButton;
@ -79,6 +80,7 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
gitUrlOverride = Root.Q<TextField>("git-url-override");
browseGitUrlButton = Root.Q<Button>("browse-git-url-button");
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
deploySourcePath = Root.Q<TextField>("deploy-source-path");
browseDeploySourceButton = Root.Q<Button>("browse-deploy-source-button");
clearDeploySourceButton = Root.Q<Button>("clear-deploy-source-button");
@ -105,6 +107,7 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
advancedSettingsFoldout.value = false;
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
UpdateDeploymentSection();
}
@ -151,6 +154,12 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
OnHttpServerCommandUpdateRequested?.Invoke();
};
devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
OnHttpServerCommandUpdateRequested?.Invoke();
});
deploySourcePath.RegisterValueChangedCallback(evt =>
{
string path = evt.newValue?.Trim();
@ -205,6 +214,7 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
}
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
UpdateDeploymentSection();
}

View File

@ -42,6 +42,13 @@
<ui:Label text="• Local: /Users/you/Projects/unity-mcp/Server" class="help-text" />
<ui:Label text="• Remote: git+https://github.com/CoplayDev/unity-mcp@v6.3.0#subdirectory=Server" class="help-text" />
<ui:Label text="Dev Mode:" class="advanced-label" style="margin-top: 10px;" />
<ui:Label text="When enabled, generated uvx commands add '--no-cache --refresh' before launching (slower startup, but avoids stale cached builds while iterating on the Server)." class="help-text" />
<ui:VisualElement class="setting-row">
<ui:Label text="Force fresh server install:" class="setting-label" />
<ui:Toggle name="dev-mode-force-refresh-toggle" class="setting-toggle" />
</ui:VisualElement>
<ui:Label text="Local Package Deployment:" class="advanced-label" style="margin-top: 12px;" />
<ui:Label text="Copy a MCPForUnity folder into this project's package location." class="help-text" />
<ui:VisualElement class="override-row">

View File

@ -30,7 +30,7 @@ keywords = ["mcp", "unity", "ai", "model context protocol", "gamedev", "unity3d"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.27.2",
"fastmcp>=2.13.0,<2.13.2",
"fastmcp==2.14.1",
"mcp>=1.16.0",
"pydantic>=2.12.5",
"tomli>=2.3.0",

View File

@ -8,6 +8,32 @@ import time
from typing import AsyncIterator, Any
from urllib.parse import urlparse
# Workaround for environments where tool signature evaluation runs with a globals
# dict that does not include common `typing` names (e.g. when annotations are strings
# and evaluated via `eval()` during schema generation).
# Making these names available in builtins avoids `NameError: Annotated/Literal/... is not defined`.
try: # pragma: no cover - startup safety guard
import builtins
import typing as _typing
_typing_names = (
"Annotated",
"Literal",
"Any",
"Union",
"Optional",
"Dict",
"List",
"Tuple",
"Set",
"FrozenSet",
)
for _name in _typing_names:
if not hasattr(builtins, _name) and hasattr(_typing, _name):
setattr(builtins, _name, getattr(_typing, _name)) # type: ignore[attr-defined]
except Exception:
pass
from fastmcp import FastMCP
from logging.handlers import RotatingFileHandler
from starlette.requests import Request
@ -242,6 +268,18 @@ Console Monitoring:
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
Payload sizing & paging (important):
- Many Unity queries can return very large JSON. Prefer **paged + summary-first** calls.
- `manage_scene(action="get_hierarchy")`:
- Use `page_size` + `cursor` and follow `next_cursor` until null.
- `page_size` is **items per page**; recommended starting point: **50**.
- `manage_gameobject(action="get_components")`:
- Start with `include_properties=false` (metadata-only) and small `page_size` (e.g. **10-25**).
- Only request `include_properties=true` when needed; keep `page_size` small (e.g. **3-10**) to bound payloads.
- `manage_asset(action="search")`:
- Use paging (`page_size`, `page_number`) and keep `page_size` modest (e.g. **25-50**) to avoid token-heavy responses.
- Keep `generate_preview=false` unless you explicitly need thumbnails (previews may include large base64 payloads).
"""
)

View File

@ -1,4 +1,8 @@
from typing import Any
import os
import sys
from core.telemetry import get_package_version
from fastmcp import Context
from services.registry import mcp_for_unity_tool
@ -50,6 +54,11 @@ def debug_request_context(ctx: Context) -> dict[str, Any]:
return {
"success": True,
"data": {
"server": {
"version": get_package_version(),
"cwd": os.getcwd(),
"argv": list(sys.argv),
},
"request_context": {
"client_id": rc_client_id,
"session_id": rc_session_id,

View File

@ -9,18 +9,22 @@ from typing import Annotated, Any, Literal
from fastmcp import Context
from services.registry import mcp_for_unity_tool
from services.tools import get_unity_instance_from_context
from services.tools.utils import parse_json_payload
from services.tools.utils import parse_json_payload, coerce_int
from transport.unity_transport import send_with_unity_instance
from transport.legacy.unity_connection import async_send_command_with_retry
@mcp_for_unity_tool(
description="Performs asset operations (import, create, modify, delete, etc.) in Unity."
description=(
"Performs asset operations (import, create, modify, delete, etc.) in Unity.\n\n"
"Tip (payload safety): for `action=\"search\"`, prefer paging (`page_size`, `page_number`) and keep "
"`generate_preview=false` (previews can add large base64 blobs)."
)
)
async def manage_asset(
ctx: Context,
action: Annotated[Literal["import", "create", "modify", "delete", "duplicate", "move", "rename", "search", "get_info", "create_folder", "get_components"], "Perform CRUD operations on assets."],
path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope."],
path: Annotated[str, "Asset path (e.g., 'Materials/MyMaterial.mat') or search scope (e.g., 'Assets')."],
asset_type: Annotated[str,
"Asset type (e.g., 'Material', 'Folder') - required for 'create'. Note: For ScriptableObjects, use manage_scriptable_object."] | None = None,
properties: Annotated[dict[str, Any] | str,
@ -28,16 +32,18 @@ async def manage_asset(
destination: Annotated[str,
"Target path for 'duplicate'/'move'."] | None = None,
generate_preview: Annotated[bool,
"Generate a preview/thumbnail for the asset when supported."] = False,
"Generate a preview/thumbnail for the asset when supported. "
"Warning: previews may include large base64 payloads; keep false unless needed."] = False,
search_pattern: Annotated[str,
"Search pattern (e.g., '*.prefab')."] | None = None,
"Search pattern (e.g., '*.prefab' or AssetDatabase filters like 't:MonoScript'). "
"Recommended: put queries like 't:MonoScript' here and set path='Assets'."] | None = None,
filter_type: Annotated[str, "Filter type for search"] | None = None,
filter_date_after: Annotated[str,
"Date after which to filter"] | None = None,
page_size: Annotated[int | float | str,
"Page size for pagination"] | None = None,
"Page size for pagination. Recommended: 25 (smaller for LLM-friendly responses)."] | None = None,
page_number: Annotated[int | float | str,
"Page number for pagination"] | None = None,
"Page number for pagination (1-based)."] | None = None,
) -> dict[str, Any]:
unity_instance = get_unity_instance_from_context(ctx)
@ -83,24 +89,32 @@ async def manage_asset(
await ctx.error(parse_error)
return {"success": False, "message": parse_error}
# Coerce numeric inputs defensively
def _coerce_int(value, default=None):
if value is None:
return default
try:
if isinstance(value, bool):
return default
if isinstance(value, int):
return int(value)
s = str(value).strip()
if s.lower() in ("", "none", "null"):
return default
return int(float(s))
except Exception:
return default
page_size = coerce_int(page_size)
page_number = coerce_int(page_number)
page_size = _coerce_int(page_size)
page_number = _coerce_int(page_number)
# --- Payload-safe normalization for common LLM mistakes (search) ---
# Unity's C# handler treats `path` as a folder scope. If a model mistakenly puts a query like
# "t:MonoScript" into `path`, Unity will consider it an invalid folder and fall back to searching
# the entire project, which is token-heavy. Normalize such cases into search_pattern + Assets scope.
action_l = (action or "").lower()
if action_l == "search":
try:
raw_path = (path or "").strip()
except (AttributeError, TypeError):
# Handle case where path is not a string despite type annotation
raw_path = ""
# If the caller put an AssetDatabase query into `path`, treat it as `search_pattern`.
if (not search_pattern) and raw_path.startswith("t:"):
search_pattern = raw_path
path = "Assets"
await ctx.info("manage_asset(search): normalized query from `path` into `search_pattern` and set path='Assets'")
# If the caller used `asset_type` to mean a search filter, map it to filter_type.
# (In Unity, filterType becomes `t:<filterType>`.)
if (not filter_type) and asset_type and isinstance(asset_type, str):
filter_type = asset_type
await ctx.info("manage_asset(search): mapped `asset_type` into `filter_type` for safer server-side filtering")
# Prepare parameters for the C# handler
params_dict = {

View File

@ -7,7 +7,7 @@ from services.registry import mcp_for_unity_tool
from services.tools import get_unity_instance_from_context
from transport.unity_transport import send_with_unity_instance
from transport.legacy.unity_connection import async_send_command_with_retry
from services.tools.utils import coerce_bool, parse_json_payload
from services.tools.utils import coerce_bool, parse_json_payload, coerce_int
@mcp_for_unity_tool(
@ -68,6 +68,11 @@ async def manage_gameobject(
# Controls whether serialization of private [SerializeField] fields is included
includeNonPublicSerialized: Annotated[bool | str,
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
# --- Paging/safety for get_components ---
page_size: Annotated[int | str, "Page size for get_components paging."] | None = None,
cursor: Annotated[int | str, "Opaque cursor for get_components paging (offset)."] | None = None,
max_components: Annotated[int | str, "Hard cap on returned components per request (safety)."] | None = None,
include_properties: Annotated[bool | str, "If true, include serialized component properties (bounded)."] | None = None,
# --- Parameters for 'duplicate' ---
new_name: Annotated[str,
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
@ -134,7 +139,12 @@ async def manage_gameobject(
search_in_children = coerce_bool(search_in_children)
search_inactive = coerce_bool(search_inactive)
includeNonPublicSerialized = coerce_bool(includeNonPublicSerialized)
include_properties = coerce_bool(include_properties)
world_space = coerce_bool(world_space, default=True)
# If coercion fails, omit these fields (None) rather than preserving invalid input.
page_size = coerce_int(page_size, default=None)
cursor = coerce_int(cursor, default=None)
max_components = coerce_int(max_components, default=None)
# Coerce 'component_properties' from JSON string to dict for client compatibility
component_properties = parse_json_payload(component_properties)
@ -194,6 +204,10 @@ async def manage_gameobject(
"searchInactive": search_inactive,
"componentName": component_name,
"includeNonPublicSerialized": includeNonPublicSerialized,
"pageSize": page_size,
"cursor": cursor,
"maxComponents": max_components,
"includeProperties": include_properties,
# Parameters for 'duplicate'
"new_name": new_name,
"offset": offset,

View File

@ -3,6 +3,7 @@ from typing import Annotated, Literal, Any
from fastmcp import Context
from services.registry import mcp_for_unity_tool
from services.tools import get_unity_instance_from_context
from services.tools.utils import coerce_int, coerce_bool
from transport.unity_transport import send_with_unity_instance
from transport.legacy.unity_connection import async_send_command_with_retry
@ -27,29 +28,27 @@ async def manage_scene(
"Unity build index (quote as string, e.g., '0')."] | None = None,
screenshot_file_name: Annotated[str, "Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None,
screenshot_super_size: Annotated[int | str, "Screenshot supersize multiplier (integer ≥1). Optional." ] | None = None,
# --- get_hierarchy paging/safety ---
parent: Annotated[str | int, "Optional parent GameObject reference (name/path/instanceID) to list direct children."] | None = None,
page_size: Annotated[int | str, "Page size for get_hierarchy paging."] | None = None,
cursor: Annotated[int | str, "Opaque cursor for paging (offset)."] | None = None,
max_nodes: Annotated[int | str, "Hard cap on returned nodes per request (safety)."] | None = None,
max_depth: Annotated[int | str, "Accepted for forward-compatibility; current paging returns a single level."] | None = None,
max_children_per_node: Annotated[int | str, "Child paging hint (safety)."] | None = None,
include_transform: Annotated[bool | str, "If true, include local transform in node summaries."] | None = None,
) -> dict[str, Any]:
# Get active instance from session state
# Removed session_state import
unity_instance = get_unity_instance_from_context(ctx)
try:
# Coerce numeric inputs defensively
def _coerce_int(value, default=None):
if value is None:
return default
try:
if isinstance(value, bool):
return default
if isinstance(value, int):
return int(value)
s = str(value).strip()
if s.lower() in ("", "none", "null"):
return default
return int(float(s))
except Exception:
return default
coerced_build_index = _coerce_int(build_index, default=None)
coerced_super_size = _coerce_int(screenshot_super_size, default=None)
coerced_build_index = coerce_int(build_index, default=None)
coerced_super_size = coerce_int(screenshot_super_size, default=None)
coerced_page_size = coerce_int(page_size, default=None)
coerced_cursor = coerce_int(cursor, default=None)
coerced_max_nodes = coerce_int(max_nodes, default=None)
coerced_max_depth = coerce_int(max_depth, default=None)
coerced_max_children_per_node = coerce_int(max_children_per_node, default=None)
coerced_include_transform = coerce_bool(include_transform, default=None)
params: dict[str, Any] = {"action": action}
if name:
@ -63,6 +62,22 @@ async def manage_scene(
if coerced_super_size is not None:
params["superSize"] = coerced_super_size
# get_hierarchy paging/safety params (optional)
if parent is not None:
params["parent"] = parent
if coerced_page_size is not None:
params["pageSize"] = coerced_page_size
if coerced_cursor is not None:
params["cursor"] = coerced_cursor
if coerced_max_nodes is not None:
params["maxNodes"] = coerced_max_nodes
if coerced_max_depth is not None:
params["maxDepth"] = coerced_max_depth
if coerced_max_children_per_node is not None:
params["maxChildrenPerNode"] = coerced_max_children_per_node
if coerced_include_transform is not None:
params["includeTransform"] = coerced_include_transform
# Use centralized retry helper with instance routing
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_scene", params)

View File

@ -6,6 +6,7 @@ from typing import Annotated, Any, Literal
from fastmcp import Context
from services.registry import mcp_for_unity_tool
from services.tools import get_unity_instance_from_context
from services.tools.utils import coerce_int, coerce_bool
from transport.unity_transport import send_with_unity_instance
from transport.legacy.unity_connection import async_send_command_with_retry
@ -38,42 +39,14 @@ async def read_console(
format = format if format is not None else 'detailed'
# Coerce booleans defensively (strings like 'true'/'false')
def _coerce_bool(value, default=None):
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
v = value.strip().lower()
if v in ("true", "1", "yes", "on"):
return True
if v in ("false", "0", "no", "off"):
return False
return bool(value)
include_stacktrace = _coerce_bool(include_stacktrace, True)
include_stacktrace = coerce_bool(include_stacktrace, default=True)
# Normalize action if it's a string
if isinstance(action, str):
action = action.lower()
# Coerce count defensively (string/float -> int)
def _coerce_int(value, default=None):
if value is None:
return default
try:
if isinstance(value, bool):
return default
if isinstance(value, int):
return int(value)
s = str(value).strip()
if s.lower() in ("", "none", "null"):
return default
return int(float(s))
except Exception:
return default
count = _coerce_int(count)
count = coerce_int(count)
# Prepare parameters for the C# handler
params_dict = {

View File

@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from models import MCPResponse
from services.registry import mcp_for_unity_tool
from services.tools import get_unity_instance_from_context
from services.tools.utils import coerce_int
from transport.unity_transport import send_with_unity_instance
from transport.legacy.unity_connection import async_send_command_with_retry
@ -54,22 +55,6 @@ async def run_tests(
) -> RunTestsResponse:
unity_instance = get_unity_instance_from_context(ctx)
# Coerce timeout defensively (string/float -> int)
def _coerce_int(value, default=None):
if value is None:
return default
try:
if isinstance(value, bool):
return default
if isinstance(value, int):
return int(value)
s = str(value).strip()
if s.lower() in ("", "none", "null"):
return default
return int(float(s))
except Exception:
return default
# Coerce string or list to list of strings
def _coerce_string_list(value) -> list[str] | None:
if value is None:
@ -82,7 +67,7 @@ async def run_tests(
return None
params: dict[str, Any] = {"mode": mode}
ts = _coerce_int(timeout_seconds)
ts = coerce_int(timeout_seconds)
if ts is not None:
params["timeoutSeconds"] = ts

View File

@ -58,3 +58,20 @@ def parse_json_payload(value: Any) -> Any:
except (json.JSONDecodeError, ValueError):
# If parsing fails, assume it was meant to be a literal string
return value
def coerce_int(value: Any, default: int | None = None) -> int | None:
"""Attempt to coerce a loosely-typed value to an integer."""
if value is None:
return default
try:
if isinstance(value, bool):
return default
if isinstance(value, int):
return value
s = str(value).strip()
if s.lower() in ("", "none", "null"):
return default
return int(float(s))
except Exception:
return default

View File

@ -0,0 +1,29 @@
import pytest
def test_debug_request_context_includes_server_diagnostics(monkeypatch):
# Import inside test so stubs in conftest are applied.
import services.tools.debug_request_context as mod
class DummyCtx:
# minimal surface for debug_request_context
request_context = None
session_id = None
client_id = None
def get_state(self, _k):
return None
# Ensure get_package_version is stable for assertion
monkeypatch.setattr(mod, "get_package_version", lambda: "9.9.9-test")
res = mod.debug_request_context(DummyCtx())
assert res.get("success") is True
data = res.get("data") or {}
server = data.get("server") or {}
assert server.get("version") == "9.9.9-test"
assert "cwd" in server
assert "argv" in server

View File

@ -32,5 +32,41 @@ async def test_manage_gameobject_boolean_and_tag_mapping(monkeypatch):
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)
assert captured["params"]["findAll"] is True
assert captured["params"]["searchInactive"] is False
@pytest.mark.asyncio
async def test_manage_gameobject_get_components_paging_params_pass_through(monkeypatch):
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["params"] = params
return {"success": True, "data": {}}
monkeypatch.setattr(
manage_go_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await manage_go_mod.manage_gameobject(
ctx=DummyContext(),
action="get_components",
target="Player",
search_method="by_name",
page_size="25",
cursor="50",
max_components="100",
include_properties="true",
)
assert resp.get("success") is True
p = captured["params"]
assert p["action"] == "get_components"
assert p["target"] == "Player"
assert p["searchMethod"] == "by_name"
assert p["pageSize"] == 25
assert p["cursor"] == 50
assert p["maxComponents"] == 100
assert p["includeProperties"] is True

View File

@ -0,0 +1,44 @@
import pytest
from .test_helpers import DummyContext
import services.tools.manage_scene as manage_scene_mod
@pytest.mark.asyncio
async def test_manage_scene_get_hierarchy_paging_params_pass_through(monkeypatch):
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["params"] = params
return {"success": True, "data": {}}
monkeypatch.setattr(
manage_scene_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await manage_scene_mod.manage_scene(
ctx=DummyContext(),
action="get_hierarchy",
parent="Player",
page_size="10",
cursor="20",
max_nodes="1000",
max_depth="6",
max_children_per_node="200",
include_transform="true",
)
assert resp.get("success") is True
p = captured["params"]
assert p["action"] == "get_hierarchy"
assert p["parent"] == "Player"
assert p["pageSize"] in (10, "10")
assert p["cursor"] in (20, "20")
assert p["maxNodes"] in (1000, "1000")
assert p["maxDepth"] in (6, "6")
assert p["maxChildrenPerNode"] in (200, "200")
assert p["includeTransform"] in (True, "true")

View File

@ -0,0 +1,33 @@
import inspect
# pyright: reportMissingImports=false
def test_manage_scene_signature_includes_paging_params():
import services.tools.manage_scene as mod
sig = inspect.signature(mod.manage_scene)
names = list(sig.parameters.keys())
# get_hierarchy paging/safety params
assert "parent" in names
assert "page_size" in names
assert "cursor" in names
assert "max_nodes" in names
assert "max_depth" in names
assert "max_children_per_node" in names
assert "include_transform" in names
def test_manage_gameobject_signature_includes_paging_params():
import services.tools.manage_gameobject as mod
sig = inspect.signature(mod.manage_gameobject)
names = list(sig.parameters.keys())
assert "page_size" in names
assert "cursor" in names
assert "max_components" in names
assert "include_properties" in names

View File

@ -35,6 +35,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
[[package]]
name = "async-timeout"
version = "5.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
]
[[package]]
name = "attrs"
version = "25.4.0"
@ -284,6 +293,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
]
[[package]]
name = "cloudpickle"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@ -436,6 +454,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]
[[package]]
name = "fakeredis"
version = "2.33.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "redis" },
{ name = "sortedcontainers" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" },
]
[package.optional-dependencies]
lua = [
{ name = "lupa" },
]
[[package]]
name = "fastapi"
version = "0.121.2"
@ -453,7 +490,7 @@ wheels = [
[[package]]
name = "fastmcp"
version = "2.13.0.2"
version = "2.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
@ -466,14 +503,16 @@ dependencies = [
{ name = "platformdirs" },
{ name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] },
{ name = "pydantic", extra = ["email"] },
{ name = "pydocket" },
{ name = "pyperclip" },
{ name = "python-dotenv" },
{ name = "rich" },
{ name = "uvicorn" },
{ name = "websockets" },
]
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" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331, upload-time = "2025-12-15T02:26:27.102Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176, upload-time = "2025-12-15T02:26:25.356Z" },
]
[[package]]
@ -657,6 +696,80 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
]
[[package]]
name = "lupa"
version = "2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" },
{ url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" },
{ url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" },
{ url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" },
{ url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" },
{ url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" },
{ url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" },
{ url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" },
{ url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" },
{ url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" },
{ url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" },
{ url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" },
{ url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" },
{ url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" },
{ url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" },
{ url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" },
{ url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" },
{ url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" },
{ url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" },
{ url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" },
{ url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" },
{ url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" },
{ url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" },
{ url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" },
{ url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" },
{ url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" },
{ url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" },
{ url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" },
{ url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" },
{ url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" },
{ url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" },
{ url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" },
{ url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" },
{ url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" },
{ url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" },
{ url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" },
{ url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" },
{ url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" },
{ url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" },
{ url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" },
{ url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" },
{ url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" },
{ url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" },
{ url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" },
{ url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" },
{ url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" },
{ url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" },
{ url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" },
{ url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" },
{ url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" },
{ url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" },
{ url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" },
{ url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" },
{ url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" },
{ url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" },
{ url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" },
{ url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" },
{ url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" },
{ url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@ -671,7 +784,7 @@ wheels = [
[[package]]
name = "mcp"
version = "1.20.0"
version = "1.25.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -685,16 +798,18 @@ dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
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" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" }
wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" },
]
[[package]]
name = "mcpforunityserver"
version = "8.2.3"
version = "8.3.0"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
@ -715,7 +830,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.104.0" },
{ name = "fastmcp", specifier = ">=2.13.0,<2.13.2" },
{ name = "fastmcp", specifier = "==2.14.1" },
{ name = "httpx", specifier = ">=0.27.2" },
{ name = "mcp", specifier = ">=1.16.0" },
{ name = "pydantic", specifier = ">=2.12.5" },
@ -756,6 +871,75 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.39.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
]
[[package]]
name = "opentelemetry-exporter-prometheus"
version = "0.60b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" },
]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.60b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.39.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.60b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
]
[[package]]
name = "packaging"
version = "25.0"
@ -801,17 +985,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "prometheus-client"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" },
]
[[package]]
name = "py-key-value-aio"
version = "0.2.8"
version = "0.3.0"
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" }
sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" },
]
[package.optional-dependencies]
@ -825,18 +1018,21 @@ keyring = [
memory = [
{ name = "cachetools" },
]
redis = [
{ name = "redis" },
]
[[package]]
name = "py-key-value-shared"
version = "0.2.8"
version = "0.3.0"
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" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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" },
{ url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" },
]
[[package]]
@ -999,6 +1195,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" },
]
[[package]]
name = "pydocket"
version = "0.16.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cloudpickle" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "fakeredis", extra = ["lua"] },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-prometheus" },
{ name = "opentelemetry-instrumentation" },
{ name = "prometheus-client" },
{ name = "py-key-value-aio", extra = ["memory", "redis"] },
{ name = "python-json-logger" },
{ name = "redis" },
{ name = "rich" },
{ name = "typer" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/c5/61dcfce4d50b66a3f09743294d37fab598b81bb0975054b7f732da9243ec/pydocket-0.16.3.tar.gz", hash = "sha256:78e9da576de09e9f3f410d2471ef1c679b7741ddd21b586c97a13872b69bd265", size = 297080, upload-time = "2025-12-23T23:37:33.32Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/94/93b7f5981aa04f922e0d9ce7326a4587866ec7e39f7c180ffcf408e66ee8/pydocket-0.16.3-py3-none-any.whl", hash = "sha256:e2b50925356e7cd535286255195458ac7bba15f25293356651b36d223db5dd7c", size = 67087, upload-time = "2025-12-23T23:37:31.829Z" },
]
[[package]]
name = "pygments"
version = "2.19.1"
@ -1072,6 +1292,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]]
name = "python-json-logger"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
@ -1176,6 +1405,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "redis"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
]
[[package]]
name = "referencing"
version = "0.36.2"
@ -1380,6 +1621,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
@ -1389,6 +1639,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "sse-starlette"
version = "2.2.1"
@ -1463,6 +1722,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
]
[[package]]
name = "typer"
version = "0.21.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
@ -1566,6 +1840,75 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
[[package]]
name = "wrapt"
version = "1.17.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" },
{ url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" },
{ url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" },
{ url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" },
{ url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" },
{ url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" },
{ url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" },
{ url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" },
{ url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" },
{ url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" },
{ url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" },
{ url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" },
{ url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" },
{ url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" },
{ url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" },
{ url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" },
{ url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" },
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"

View File

@ -32,6 +32,8 @@ namespace MCPForUnityTests.Editor.Helpers
private string _originalGitOverride;
private bool _hadHttpTransport;
private bool _originalHttpTransport;
private bool _hadDevForceRefresh;
private bool _originalDevForceRefresh;
private IPlatformService _originalPlatformService;
[OneTimeSetUp]
@ -41,6 +43,8 @@ namespace MCPForUnityTests.Editor.Helpers
_originalGitOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty);
_hadHttpTransport = EditorPrefs.HasKey(EditorPrefKeys.UseHttpTransport);
_originalHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
_hadDevForceRefresh = EditorPrefs.HasKey(EditorPrefKeys.DevModeForceServerRefresh);
_originalDevForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
_originalPlatformService = MCPServiceLocator.Platform;
}
@ -51,6 +55,9 @@ namespace MCPForUnityTests.Editor.Helpers
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
// Default to stdio mode for existing tests unless specified otherwise
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
// Ensure deterministic uvx args ordering for these tests regardless of editor settings
// (dev-mode inserts --no-cache/--refresh, which changes the first args).
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
}
[TearDown]
@ -66,6 +73,10 @@ namespace MCPForUnityTests.Editor.Helpers
{
MCPServiceLocator.Register<IPlatformService>(_originalPlatformService);
}
else
{
MCPServiceLocator.Register<IPlatformService>(new PlatformService());
}
}
[OneTimeTearDown]
@ -88,6 +99,15 @@ namespace MCPForUnityTests.Editor.Helpers
{
EditorPrefs.DeleteKey(EditorPrefKeys.UseHttpTransport);
}
if (_hadDevForceRefresh)
{
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, _originalDevForceRefresh);
}
else
{
EditorPrefs.DeleteKey(EditorPrefKeys.DevModeForceServerRefresh);
}
}
[Test]

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
@ -68,6 +69,81 @@ namespace MCPForUnityTests.Editor.Tools
}
}
[Test]
public void GetComponents_ReturnsPagedMetadataByDefault()
{
// Arrange
testGameObject.AddComponent<Rigidbody>();
testGameObject.AddComponent<BoxCollider>();
var p = new JObject
{
["action"] = "get_components",
["target"] = testGameObject.name,
["searchMethod"] = "by_name",
["pageSize"] = 2
};
// Act
var raw = ManageGameObject.HandleCommand(p);
var result = raw as JObject ?? JObject.FromObject(raw);
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Expected data payload object.");
Assert.AreEqual(false, data.Value<bool>("includeProperties"));
var items = data["items"] as JArray;
Assert.IsNotNull(items, "Expected items array.");
Assert.AreEqual(2, items.Count, "Expected exactly pageSize items.");
var first = items[0] as JObject;
Assert.IsNotNull(first, "Expected item to be an object.");
Assert.IsNotNull(first["typeName"]);
Assert.IsNotNull(first["instanceID"]);
Assert.IsNull(first["properties"], "Metadata response should not include heavy serialized properties by default.");
}
[Test]
public void GetComponents_CanIncludePropertiesButStillPages()
{
// Arrange
testGameObject.AddComponent<Rigidbody>();
testGameObject.AddComponent<BoxCollider>();
var p = new JObject
{
["action"] = "get_components",
["target"] = testGameObject.name,
["searchMethod"] = "by_name",
["pageSize"] = 2,
["includeProperties"] = true
};
// Act
var raw = ManageGameObject.HandleCommand(p);
var result = raw as JObject ?? JObject.FromObject(raw);
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
Assert.AreEqual(true, data.Value<bool>("includeProperties"));
var items = data["items"] as JArray;
Assert.IsNotNull(items);
Assert.IsTrue(items.Count > 0);
var first = items[0] as JObject;
Assert.IsNotNull(first);
Assert.IsNotNull(first["typeName"]);
Assert.IsNotNull(first["instanceID"]);
// Heuristic: property-including payload should have more than just typeName/instanceID.
Assert.Greater(first.Properties().Count(), 2, "Expected richer component payload when includeProperties=true.");
}
[Test]
public void ComponentResolver_Integration_WorksWithRealComponents()
{

View File

@ -0,0 +1,105 @@
using NUnit.Framework;
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageSceneHierarchyPagingTests
{
private GameObject _root;
private readonly System.Collections.Generic.List<GameObject> _created = new System.Collections.Generic.List<GameObject>();
[TearDown]
public void TearDown()
{
for (int i = 0; i < _created.Count; i++)
{
if (_created[i] != null) Object.DestroyImmediate(_created[i]);
}
_created.Clear();
if (_root != null)
{
Object.DestroyImmediate(_root);
_root = null;
}
}
[Test]
public void GetHierarchy_PaginatesRoots_AndSupportsChildrenPaging()
{
// Arrange: create many roots so paging must occur.
// Note: Keep counts modest to avoid slowing EditMode tests.
const int rootCount = 40;
for (int i = 0; i < rootCount; i++)
{
_created.Add(new GameObject($"HS_Root_{i:D2}"));
}
_root = new GameObject("HS_Parent");
for (int i = 0; i < 15; i++)
{
var child = new GameObject($"HS_Child_{i:D2}");
child.transform.SetParent(_root.transform);
}
// Act: request a small page to force truncation
var p1 = new JObject
{
["action"] = "get_hierarchy",
["pageSize"] = 10,
};
var raw1 = ManageScene.HandleCommand(p1);
var res1 = raw1 as JObject ?? JObject.FromObject(raw1);
// Assert: envelope success + payload shape
Assert.IsTrue(res1.Value<bool>("success"), res1.ToString());
var data1 = res1["data"] as JObject;
Assert.IsNotNull(data1, "Expected data payload to be an object.");
Assert.AreEqual("roots", data1.Value<string>("scope"));
Assert.AreEqual(true, data1.Value<bool>("truncated"), "Expected truncation when pageSize < root count.");
Assert.IsNotNull(data1["next_cursor"], "Expected next_cursor when truncated.");
var items1 = data1["items"] as JArray;
Assert.IsNotNull(items1, "Expected items array.");
Assert.AreEqual(10, items1.Count, "Expected exactly pageSize items returned.");
// Act: fetch next page of roots using next_cursor
var cursor = data1.Value<string>("next_cursor");
var p2 = new JObject
{
["action"] = "get_hierarchy",
["pageSize"] = 10,
["cursor"] = cursor,
};
var raw2 = ManageScene.HandleCommand(p2);
var res2 = raw2 as JObject ?? JObject.FromObject(raw2);
Assert.IsTrue(res2.Value<bool>("success"), res2.ToString());
var data2 = res2["data"] as JObject;
Assert.IsNotNull(data2);
var items2 = data2["items"] as JArray;
Assert.IsNotNull(items2);
Assert.AreEqual(10, items2.Count);
// Act: page children of a specific parent via 'parent' param (instance ID)
var pChildren = new JObject
{
["action"] = "get_hierarchy",
["parent"] = _root.GetInstanceID(),
["pageSize"] = 7,
};
var rawChildren = ManageScene.HandleCommand(pChildren);
var resChildren = rawChildren as JObject ?? JObject.FromObject(rawChildren);
Assert.IsTrue(resChildren.Value<bool>("success"), resChildren.ToString());
var dataChildren = resChildren["data"] as JObject;
Assert.IsNotNull(dataChildren);
Assert.AreEqual("children", dataChildren.Value<string>("scope"));
Assert.AreEqual(true, dataChildren.Value<bool>("truncated"));
Assert.IsNotNull(dataChildren["next_cursor"]);
var childItems = dataChildren["items"] as JArray;
Assert.IsNotNull(childItems);
Assert.AreEqual(7, childItems.Count);
}
}
}

View File

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

View File

@ -3,6 +3,7 @@ using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using System.Threading;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools;
using MCPForUnityTests.Editor.Tools.Fixtures;
@ -22,6 +23,7 @@ namespace MCPForUnityTests.Editor.Tools
[SetUp]
public void SetUp()
{
WaitForUnityReady();
EnsureFolder("Assets/Temp");
// Start from a clean slate every time (prevents intermittent setup failures).
if (AssetDatabase.IsValidFolder(TempRoot))
@ -45,6 +47,7 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.CreateAsset(new Material(shader), _matBPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
WaitForUnityReady();
}
[TearDown]
@ -304,6 +307,22 @@ namespace MCPForUnityTests.Editor.Tools
{
return result as JObject ?? JObject.FromObject(result);
}
private static void WaitForUnityReady(double timeoutSeconds = 30.0)
{
// Some EditMode tests trigger script compilation/domain reload. Tools like ManageScriptableObject
// intentionally return "compiling_or_reloading" during these windows. Wait until Unity is stable
// to make tests deterministic.
double start = EditorApplication.timeSinceStartup;
while (EditorApplication.isCompiling || EditorApplication.isUpdating)
{
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
{
Assert.Fail($"Timed out waiting for Unity to finish compiling/updating (>{timeoutSeconds:0.0}s).");
}
Thread.Sleep(50);
}
}
}
}

View File

@ -73,6 +73,7 @@ Use the MCP for Unity Editor window (Window > MCP for Unity) and open **Advanced
- **UV/UVX Path Override**: Point the UI to a specific `uv`/`uvx` executable (e.g., from a custom install) when PATH resolution is wrong. Clear to fall back to auto-discovery.
- **Server Source Override**: Set a local folder or git URL for the Python server (`uvx --from <url> mcp-for-unity`). Clear to use the packaged default.
- **Dev Mode (Force fresh server install)**: When enabled, generated `uvx` commands add `--no-cache --refresh` before launching. This is slower, but avoids accidentally running a stale cached build while iterating on `Server/`.
- **Local Package Deployment**: Pick a local `MCPForUnity` folder (must contain `Editor/` and `Runtime/`) and click **Deploy to Project** to copy it over the currently installed package path (from `Packages/manifest.json` / Package Manager). A timestamped backup is stored under `Library/MCPForUnityDeployBackups`, and **Restore Last Backup** reverts the last deploy.
Tips:
@ -168,6 +169,41 @@ To find it reliably:
Note: In recent builds, the Python server sources are also bundled inside the package under `Server`. This is handy for local testing or pointing MCP clients directly at the packaged server.
## Payload sizing & paging defaults (recommended)
Some Unity tool calls can return *very large* JSON payloads (deep hierarchies, components with full serialized properties). To keep MCP responses bounded and avoid Unity freezes/crashes, prefer **paged + summary-first** reads and fetch full properties only when needed.
### `manage_scene(action="get_hierarchy")`
- **Default behavior**: returns a **paged summary** of either root GameObjects (no `parent`) or direct children (`parent` specified). It does **not** inline full recursive subtrees.
- **Paging params**:
- **`page_size`**: defaults to **50**, clamped to **1..500**
- **`cursor`**: defaults to **0**
- **`next_cursor`**: returned as a **string** when more results remain; `null` when complete
- **Other safety knobs**:
- **`max_nodes`**: defaults to **1000**, clamped to **1..5000**
- **`include_transform`**: defaults to **false**
### `manage_gameobject(action="get_components")`
- **Default behavior**: returns **paged component metadata** only (`typeName`, `instanceID`).
- **Paging params**:
- **`page_size`**: defaults to **25**, clamped to **1..200**
- **`cursor`**: defaults to **0**
- **`max_components`**: defaults to **50**, clamped to **1..500**
- **`next_cursor`**: returned as a **string** when more results remain; `null` when complete
- **Properties-on-demand**:
- **`include_properties`** defaults to **false**
- When `include_properties=true`, the implementation enforces a conservative response-size budget (roughly **~250KB** of JSON text) and may return fewer than `page_size` items; use `next_cursor` to continue.
### Practical defaults (what we recommend in prompts/tests)
- **Hierarchy roots**: start with `page_size=50` and follow `next_cursor` (usually 12 calls for big scenes).
- **Children**: page by `parent` with `page_size=10..50` (depending on expected breadth).
- **Components**:
- Start with `include_properties=false` and `page_size=10..25`
- When you need full properties, keep `include_properties=true` with a **small** `page_size` (e.g. **3..10**) to bound peak payload sizes.
## MCP Bridge Stress Test
An on-demand stress utility exercises the MCP bridge with multiple concurrent clients while triggering real script reloads via immediate script edits (no menu calls required).