Skip to content

Commit 1e43fb5

Browse files
committed
fix: repair instance routing and simplify get_unity_instance_from_context
PROBLEM: Instance routing was failing - scripts went to wrong Unity instances. Script1 (intended: ramble) -> went to UnityMCPTests ❌ Script2 (intended: UnityMCPTests) -> went to ramble ❌ ROOT CAUSE: Two incompatible approaches for accessing active instance: 1. Middleware: ctx.set_state() / ctx.get_state() - used by most tools 2. Legacy: ctx.request_context.meta - used by script tools Script tools were reading from wrong location, middleware had no effect. FIX: 1. Updated get_unity_instance_from_context() to read from ctx.get_state() 2. Removed legacy request_context.meta code path (98 lines removed) 3. Single source of truth: middleware state only TESTING: - Added comprehensive test suite (21 tests) covering all scenarios - Tests middleware state management, session isolation, race conditions - Tests reproduce exact 4-script failure scenario - All 88 tests pass (76 passed + 5 skipped + 7 xpassed) - Verified fix with live 4-script test: 100% success rate Files changed: - Server/tools/__init__.py: Simplified from 75 lines to 15 lines - MCPForUnity/UnityMcpServer~/src/tools/__init__.py: Same simplification - tests/test_instance_routing_comprehensive.py: New comprehensive test suite
1 parent 3f7deeb commit 1e43fb5

File tree

3 files changed

+358
-98
lines changed

3 files changed

+358
-98
lines changed

MCPForUnity/UnityMcpServer~/src/tools/__init__.py

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -65,57 +65,15 @@ def get_unity_instance_from_context(
6565
ctx: Context,
6666
key: str = "unity_instance",
6767
) -> str | None:
68-
"""Extract the unity_instance value from the request metadata if present."""
69-
70-
request_context = getattr(ctx, "request_context", None)
71-
if request_context is None:
72-
return None
73-
74-
try:
75-
meta = request_context.meta
76-
except ValueError:
77-
return None
78-
79-
if meta is None:
80-
return None
81-
82-
# 1. Direct attribute access (common for extra='allow')
83-
value = getattr(meta, key, None)
84-
if value:
85-
return value
86-
87-
# 2. Pydantic "model_extra" storage for dynamic fields
88-
model_extra: dict[str, Any] | None = getattr(meta, "model_extra", None) # type: ignore[attr-defined]
89-
if isinstance(model_extra, dict):
90-
value = model_extra.get(key)
91-
if value:
92-
return value
93-
94-
# 3. Items stored as dict (defensive)
95-
if isinstance(meta, dict):
96-
value = meta.get(key)
97-
if value:
98-
return value
99-
100-
# 4. Fall back to model dump (exclude Nones)
101-
dump_fn = getattr(meta, "model_dump", None)
102-
if callable(dump_fn):
103-
try:
104-
data = dump_fn(exclude_none=True)
105-
if isinstance(data, dict):
106-
value = data.get(key)
107-
if value:
108-
return value
109-
except Exception: # pragma: no cover - defensive
110-
pass
68+
"""Extract the unity_instance value from middleware state.
11169
112-
# 5. Final fallback for mapping-like objects
113-
get_fn = getattr(meta, "get", None)
114-
if callable(get_fn):
70+
The instance is set via the set_active_instance tool and injected into
71+
request state by UnityInstanceMiddleware.
72+
"""
73+
get_state_fn = getattr(ctx, "get_state", None)
74+
if callable(get_state_fn):
11575
try:
116-
value = get_fn(key, None)
117-
if value:
118-
return value
76+
return get_state_fn(key)
11977
except Exception: # pragma: no cover - defensive
12078
pass
12179

Server/tools/__init__.py

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -65,57 +65,15 @@ def get_unity_instance_from_context(
6565
ctx: Context,
6666
key: str = "unity_instance",
6767
) -> str | None:
68-
"""Extract the unity_instance value from the request metadata if present."""
69-
70-
request_context = getattr(ctx, "request_context", None)
71-
if request_context is None:
72-
return None
73-
74-
try:
75-
meta = request_context.meta
76-
except ValueError:
77-
return None
78-
79-
if meta is None:
80-
return None
81-
82-
# 1. Direct attribute access (common for extra='allow')
83-
value = getattr(meta, key, None)
84-
if value:
85-
return value
86-
87-
# 2. Pydantic "model_extra" storage for dynamic fields
88-
model_extra: dict[str, Any] | None = getattr(meta, "model_extra", None) # type: ignore[attr-defined]
89-
if isinstance(model_extra, dict):
90-
value = model_extra.get(key)
91-
if value:
92-
return value
93-
94-
# 3. Items stored as dict (defensive)
95-
if isinstance(meta, dict):
96-
value = meta.get(key)
97-
if value:
98-
return value
99-
100-
# 4. Fall back to model dump (exclude Nones)
101-
dump_fn = getattr(meta, "model_dump", None)
102-
if callable(dump_fn):
103-
try:
104-
data = dump_fn(exclude_none=True)
105-
if isinstance(data, dict):
106-
value = data.get(key)
107-
if value:
108-
return value
109-
except Exception: # pragma: no cover - defensive
110-
pass
68+
"""Extract the unity_instance value from middleware state.
11169
112-
# 5. Final fallback for mapping-like objects
113-
get_fn = getattr(meta, "get", None)
114-
if callable(get_fn):
70+
The instance is set via the set_active_instance tool and injected into
71+
request state by UnityInstanceMiddleware.
72+
"""
73+
get_state_fn = getattr(ctx, "get_state", None)
74+
if callable(get_state_fn):
11575
try:
116-
value = get_fn(key, None)
117-
if value:
118-
return value
76+
return get_state_fn(key)
11977
except Exception: # pragma: no cover - defensive
12078
pass
12179

0 commit comments

Comments
 (0)