2025-10-25 12:53:53 +08:00
import base64
import os
from typing import Annotated , Any , Literal
from urllib . parse import urlparse , unquote
from fastmcp import FastMCP , Context
from registry import mcp_for_unity_tool
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
from tools import get_unity_instance_from_context , send_with_unity_instance
2025-10-25 12:53:53 +08:00
import unity_connection
def _split_uri ( uri : str ) - > tuple [ str , str ] :
""" Split an incoming URI or path into (name, directory) suitable for Unity.
Rules :
- unity : / / path / Assets / . . . → keep as Assets - relative ( after decode / normalize )
- file : / / . . . → percent - decode , normalize , strip host and leading slashes ,
then , if any ' Assets ' segment exists , return path relative to that ' Assets ' root .
Otherwise , fall back to original name / dir behavior .
- plain paths → decode / normalize separators ; if they contain an ' Assets ' segment ,
return relative to ' Assets ' .
"""
raw_path : str
if uri . startswith ( " unity://path/ " ) :
raw_path = uri [ len ( " unity://path/ " ) : ]
elif uri . startswith ( " file:// " ) :
parsed = urlparse ( uri )
host = ( parsed . netloc or " " ) . strip ( )
p = parsed . path or " "
# UNC: file://server/share/... -> //server/share/...
if host and host . lower ( ) != " localhost " :
p = f " // { host } { p } "
# Use percent-decoded path, preserving leading slashes
raw_path = unquote ( p )
else :
raw_path = uri
# Percent-decode any residual encodings and normalize separators
raw_path = unquote ( raw_path ) . replace ( " \\ " , " / " )
# Strip leading slash only for Windows drive-letter forms like "/C:/..."
if os . name == " nt " and len ( raw_path ) > = 3 and raw_path [ 0 ] == " / " and raw_path [ 2 ] == " : " :
raw_path = raw_path [ 1 : ]
# Normalize path (collapse ../, ./)
norm = os . path . normpath ( raw_path ) . replace ( " \\ " , " / " )
# If an 'Assets' segment exists, compute path relative to it (case-insensitive)
parts = [ p for p in norm . split ( " / " ) if p not in ( " " , " . " ) ]
idx = next ( ( i for i , seg in enumerate ( parts )
if seg . lower ( ) == " assets " ) , None )
assets_rel = " / " . join ( parts [ idx : ] ) if idx is not None else None
effective_path = assets_rel if assets_rel else norm
# For POSIX absolute paths outside Assets, drop the leading '/'
# to return a clean relative-like directory (e.g., '/tmp' -> 'tmp').
if effective_path . startswith ( " / " ) :
effective_path = effective_path [ 1 : ]
name = os . path . splitext ( os . path . basename ( effective_path ) ) [ 0 ]
directory = os . path . dirname ( effective_path )
return name , directory
@mcp_for_unity_tool ( description = (
""" Apply small text edits to a C# script identified by URI.
IMPORTANT : This tool replaces EXACT character positions . Always verify content at target lines / columns BEFORE editing !
RECOMMENDED WORKFLOW :
1. First call resources / read with start_line / line_count to verify exact content
2. Count columns carefully ( or use find_in_file to locate patterns )
3. Apply your edit with precise coordinates
4. Consider script_apply_edits with anchors for safer pattern - based replacements
Notes :
- For method / class operations , use script_apply_edits ( safer , structured edits )
- For pattern - based replacements , consider anchor operations in script_apply_edits
- Lines , columns are 1 - indexed
- Tabs count as 1 column """
) )
def apply_text_edits (
ctx : Context ,
uri : Annotated [ str , " URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/... " ] ,
edits : Annotated [ list [ dict [ str , Any ] ] , " List of edits to apply to the script, i.e. a list of { startLine,startCol,endLine,endCol,newText} (1-indexed!) " ] ,
precondition_sha256 : Annotated [ str ,
" Optional SHA256 of the script to edit, used to prevent concurrent edits " ] | None = None ,
strict : Annotated [ bool ,
" Optional strict flag, used to enforce strict mode " ] | None = None ,
options : Annotated [ dict [ str , Any ] ,
" Optional options, used to pass additional options to the script editor " ] | None = None ,
) - > dict [ str , Any ] :
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
unity_instance = get_unity_instance_from_context ( ctx )
ctx . info ( f " Processing apply_text_edits: { uri } (unity_instance= { unity_instance or ' default ' } ) " )
2025-10-25 12:53:53 +08:00
name , directory = _split_uri ( uri )
# Normalize common aliases/misuses for resilience:
# - Accept LSP-style range objects: {range:{start:{line,character}, end:{...}}, newText|text}
# - Accept index ranges as a 2-int array: {range:[startIndex,endIndex], text}
# If normalization is required, read current contents to map indices -> 1-based line/col.
def _needs_normalization ( arr : list [ dict [ str , Any ] ] ) - > bool :
for e in arr or [ ] :
if ( " startLine " not in e ) or ( " startCol " not in e ) or ( " endLine " not in e ) or ( " endCol " not in e ) or ( " newText " not in e and " text " in e ) :
return True
return False
normalized_edits : list [ dict [ str , Any ] ] = [ ]
warnings : list [ str ] = [ ]
if _needs_normalization ( edits ) :
# Read file to support index->line/col conversion when needed
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
read_resp = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
{
" action " : " read " ,
" name " : name ,
" path " : directory ,
} ,
)
2025-10-25 12:53:53 +08:00
if not ( isinstance ( read_resp , dict ) and read_resp . get ( " success " ) ) :
return read_resp if isinstance ( read_resp , dict ) else { " success " : False , " message " : str ( read_resp ) }
data = read_resp . get ( " data " , { } )
contents = data . get ( " contents " )
if not contents and data . get ( " contentsEncoded " ) :
try :
contents = base64 . b64decode ( data . get ( " encodedContents " , " " ) . encode (
" utf-8 " ) ) . decode ( " utf-8 " , " replace " )
except Exception :
contents = contents or " "
# Helper to map 0-based character index to 1-based line/col
def line_col_from_index ( idx : int ) - > tuple [ int , int ] :
if idx < = 0 :
return 1 , 1
# Count lines up to idx and position within line
nl_count = contents . count ( " \n " , 0 , idx )
line = nl_count + 1
last_nl = contents . rfind ( " \n " , 0 , idx )
col = ( idx - ( last_nl + 1 ) ) + 1 if last_nl > = 0 else idx + 1
return line , col
for e in edits or [ ] :
e2 = dict ( e )
# Map text->newText if needed
if " newText " not in e2 and " text " in e2 :
e2 [ " newText " ] = e2 . pop ( " text " )
if " startLine " in e2 and " startCol " in e2 and " endLine " in e2 and " endCol " in e2 :
# Guard: explicit fields must be 1-based.
zero_based = False
for k in ( " startLine " , " startCol " , " endLine " , " endCol " ) :
try :
if int ( e2 . get ( k , 1 ) ) < 1 :
zero_based = True
except Exception :
pass
if zero_based :
if strict :
return { " success " : False , " code " : " zero_based_explicit_fields " , " message " : " Explicit line/col fields are 1-based; received zero-based. " , " data " : { " normalizedEdits " : normalized_edits } }
# Normalize by clamping to 1 and warn
for k in ( " startLine " , " startCol " , " endLine " , " endCol " ) :
try :
if int ( e2 . get ( k , 1 ) ) < 1 :
e2 [ k ] = 1
except Exception :
pass
warnings . append (
" zero_based_explicit_fields_normalized " )
normalized_edits . append ( e2 )
continue
rng = e2 . get ( " range " )
if isinstance ( rng , dict ) :
# LSP style: 0-based
s = rng . get ( " start " , { } )
t = rng . get ( " end " , { } )
e2 [ " startLine " ] = int ( s . get ( " line " , 0 ) ) + 1
e2 [ " startCol " ] = int ( s . get ( " character " , 0 ) ) + 1
e2 [ " endLine " ] = int ( t . get ( " line " , 0 ) ) + 1
e2 [ " endCol " ] = int ( t . get ( " character " , 0 ) ) + 1
e2 . pop ( " range " , None )
normalized_edits . append ( e2 )
continue
if isinstance ( rng , ( list , tuple ) ) and len ( rng ) == 2 :
try :
a = int ( rng [ 0 ] )
b = int ( rng [ 1 ] )
if b < a :
a , b = b , a
sl , sc = line_col_from_index ( a )
el , ec = line_col_from_index ( b )
e2 [ " startLine " ] = sl
e2 [ " startCol " ] = sc
e2 [ " endLine " ] = el
e2 [ " endCol " ] = ec
e2 . pop ( " range " , None )
normalized_edits . append ( e2 )
continue
except Exception :
pass
# Could not normalize this edit
return {
" success " : False ,
" code " : " missing_field " ,
" message " : " apply_text_edits requires startLine/startCol/endLine/endCol/newText or a normalizable ' range ' " ,
" data " : { " expected " : [ " startLine " , " startCol " , " endLine " , " endCol " , " newText " ] , " got " : e }
}
else :
# Even when edits appear already in explicit form, validate 1-based coordinates.
normalized_edits = [ ]
for e in edits or [ ] :
e2 = dict ( e )
has_all = all ( k in e2 for k in (
" startLine " , " startCol " , " endLine " , " endCol " ) )
if has_all :
zero_based = False
for k in ( " startLine " , " startCol " , " endLine " , " endCol " ) :
try :
if int ( e2 . get ( k , 1 ) ) < 1 :
zero_based = True
except Exception :
pass
if zero_based :
if strict :
return { " success " : False , " code " : " zero_based_explicit_fields " , " message " : " Explicit line/col fields are 1-based; received zero-based. " , " data " : { " normalizedEdits " : [ e2 ] } }
for k in ( " startLine " , " startCol " , " endLine " , " endCol " ) :
try :
if int ( e2 . get ( k , 1 ) ) < 1 :
e2 [ k ] = 1
except Exception :
pass
if " zero_based_explicit_fields_normalized " not in warnings :
warnings . append (
" zero_based_explicit_fields_normalized " )
normalized_edits . append ( e2 )
# Preflight: detect overlapping ranges among normalized line/col spans
def _pos_tuple ( e : dict [ str , Any ] , key_start : bool ) - > tuple [ int , int ] :
return (
int ( e . get ( " startLine " , 1 ) ) if key_start else int (
e . get ( " endLine " , 1 ) ) ,
int ( e . get ( " startCol " , 1 ) ) if key_start else int (
e . get ( " endCol " , 1 ) ) ,
)
def _le ( a : tuple [ int , int ] , b : tuple [ int , int ] ) - > bool :
return a [ 0 ] < b [ 0 ] or ( a [ 0 ] == b [ 0 ] and a [ 1 ] < = b [ 1 ] )
# Consider only true replace ranges (non-zero length). Pure insertions (zero-width) don't overlap.
spans = [ ]
for e in normalized_edits or [ ] :
try :
s = _pos_tuple ( e , True )
t = _pos_tuple ( e , False )
if s != t :
spans . append ( ( s , t ) )
except Exception :
# If coordinates missing or invalid, let the server validate later
pass
if spans :
spans_sorted = sorted ( spans , key = lambda p : ( p [ 0 ] [ 0 ] , p [ 0 ] [ 1 ] ) )
for i in range ( 1 , len ( spans_sorted ) ) :
prev_end = spans_sorted [ i - 1 ] [ 1 ]
curr_start = spans_sorted [ i ] [ 0 ]
# Overlap if prev_end > curr_start (strict), i.e., not prev_end <= curr_start
if not _le ( prev_end , curr_start ) :
conflicts = [ {
" startA " : { " line " : spans_sorted [ i - 1 ] [ 0 ] [ 0 ] , " col " : spans_sorted [ i - 1 ] [ 0 ] [ 1 ] } ,
" endA " : { " line " : spans_sorted [ i - 1 ] [ 1 ] [ 0 ] , " col " : spans_sorted [ i - 1 ] [ 1 ] [ 1 ] } ,
" startB " : { " line " : spans_sorted [ i ] [ 0 ] [ 0 ] , " col " : spans_sorted [ i ] [ 0 ] [ 1 ] } ,
" endB " : { " line " : spans_sorted [ i ] [ 1 ] [ 0 ] , " col " : spans_sorted [ i ] [ 1 ] [ 1 ] } ,
} ]
return { " success " : False , " code " : " overlap " , " data " : { " status " : " overlap " , " conflicts " : conflicts } }
# Note: Do not auto-compute precondition if missing; callers should supply it
# via mcp__unity__get_sha or a prior read. This avoids hidden extra calls and
# preserves existing call-count expectations in clients/tests.
# Default options: for multi-span batches, prefer atomic to avoid mid-apply imbalance
opts : dict [ str , Any ] = dict ( options or { } )
try :
if len ( normalized_edits ) > 1 and " applyMode " not in opts :
opts [ " applyMode " ] = " atomic "
except Exception :
pass
# Support optional debug preview for span-by-span simulation without write
if opts . get ( " debug_preview " ) :
try :
import difflib
# Apply locally to preview final result
lines = [ ]
# Build an indexable original from a read if we normalized from read; otherwise skip
prev = " "
# We cannot guarantee file contents here without a read; return normalized spans only
return {
" success " : True ,
" message " : " Preview only (no write) " ,
" data " : {
" normalizedEdits " : normalized_edits ,
" preview " : True
}
}
except Exception as e :
return { " success " : False , " code " : " preview_failed " , " message " : f " debug_preview failed: { e } " , " data " : { " normalizedEdits " : normalized_edits } }
params = {
" action " : " apply_text_edits " ,
" name " : name ,
" path " : directory ,
" edits " : normalized_edits ,
" precondition_sha256 " : precondition_sha256 ,
" options " : opts ,
}
params = { k : v for k , v in params . items ( ) if v is not None }
2025-11-06 04:08:59 +08:00
resp = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
params ,
)
2025-10-25 12:53:53 +08:00
if isinstance ( resp , dict ) :
data = resp . setdefault ( " data " , { } )
data . setdefault ( " normalizedEdits " , normalized_edits )
if warnings :
data . setdefault ( " warnings " , warnings )
if resp . get ( " success " ) and ( options or { } ) . get ( " force_sentinel_reload " ) :
# Optional: flip sentinel via menu if explicitly requested
try :
import threading
import time
import json
import glob
import os
def _latest_status ( ) - > dict | None :
try :
files = sorted ( glob . glob ( os . path . expanduser (
" ~/.unity-mcp/unity-mcp-status-*.json " ) ) , key = os . path . getmtime , reverse = True )
if not files :
return None
with open ( files [ 0 ] , " r " ) as f :
return json . loads ( f . read ( ) )
except Exception :
return None
def _flip_async ( ) :
try :
time . sleep ( 0.1 )
st = _latest_status ( )
if st and st . get ( " reloading " ) :
return
unity_connection . send_command_with_retry (
" execute_menu_item " ,
{ " menuPath " : " MCP/Flip Reload Sentinel " } ,
max_retries = 0 ,
retry_ms = 0 ,
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
instance_id = unity_instance ,
2025-10-25 12:53:53 +08:00
)
except Exception :
pass
threading . Thread ( target = _flip_async , daemon = True ) . start ( )
except Exception :
pass
return resp
return resp
return { " success " : False , " message " : str ( resp ) }
@mcp_for_unity_tool ( description = ( " Create a new C# script at the given project path. " ) )
def create_script (
ctx : Context ,
path : Annotated [ str , " Path under Assets/ to create the script at, e.g., ' Assets/Scripts/My.cs ' " ] ,
contents : Annotated [ str , " Contents of the script to create. Note, this is Base64 encoded over transport. " ] ,
script_type : Annotated [ str , " Script type (e.g., ' C# ' ) " ] | None = None ,
namespace : Annotated [ str , " Namespace for the script " ] | None = None ,
) - > dict [ str , Any ] :
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
unity_instance = get_unity_instance_from_context ( ctx )
ctx . info ( f " Processing create_script: { path } (unity_instance= { unity_instance or ' default ' } ) " )
2025-10-25 12:53:53 +08:00
name = os . path . splitext ( os . path . basename ( path ) ) [ 0 ]
directory = os . path . dirname ( path )
# Local validation to avoid round-trips on obviously bad input
norm_path = os . path . normpath (
( path or " " ) . replace ( " \\ " , " / " ) ) . replace ( " \\ " , " / " )
if not directory or directory . split ( " / " ) [ 0 ] . lower ( ) != " assets " :
return { " success " : False , " code " : " path_outside_assets " , " message " : f " path must be under ' Assets/ ' ; got ' { path } ' . " }
if " .. " in norm_path . split ( " / " ) or norm_path . startswith ( " / " ) :
return { " success " : False , " code " : " bad_path " , " message " : " path must not contain traversal or be absolute. " }
if not name :
return { " success " : False , " code " : " bad_path " , " message " : " path must include a script file name. " }
if not norm_path . lower ( ) . endswith ( " .cs " ) :
return { " success " : False , " code " : " bad_extension " , " message " : " script file must end with .cs. " }
params : dict [ str , Any ] = {
" action " : " create " ,
" name " : name ,
" path " : directory ,
" namespace " : namespace ,
" scriptType " : script_type ,
}
if contents :
params [ " encodedContents " ] = base64 . b64encode (
contents . encode ( " utf-8 " ) ) . decode ( " utf-8 " )
params [ " contentsEncoded " ] = True
params = { k : v for k , v in params . items ( ) if v is not None }
2025-11-06 04:08:59 +08:00
resp = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
params ,
)
2025-10-25 12:53:53 +08:00
return resp if isinstance ( resp , dict ) else { " success " : False , " message " : str ( resp ) }
@mcp_for_unity_tool ( description = ( " Delete a C# script by URI or Assets-relative path. " ) )
def delete_script (
ctx : Context ,
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
uri : Annotated [ str , " URI of the script to delete under Assets/ directory, unity://path/Assets/... or file://... or Assets/... " ] ,
2025-10-25 12:53:53 +08:00
) - > dict [ str , Any ] :
""" Delete a C# script by URI. """
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
unity_instance = get_unity_instance_from_context ( ctx )
ctx . info ( f " Processing delete_script: { uri } (unity_instance= { unity_instance or ' default ' } ) " )
2025-10-25 12:53:53 +08:00
name , directory = _split_uri ( uri )
if not directory or directory . split ( " / " ) [ 0 ] . lower ( ) != " assets " :
return { " success " : False , " code " : " path_outside_assets " , " message " : " URI must resolve under ' Assets/ ' . " }
params = { " action " : " delete " , " name " : name , " path " : directory }
2025-11-06 04:08:59 +08:00
resp = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
params ,
)
2025-10-25 12:53:53 +08:00
return resp if isinstance ( resp , dict ) else { " success " : False , " message " : str ( resp ) }
@mcp_for_unity_tool ( description = ( " Validate a C# script and return diagnostics. " ) )
def validate_script (
ctx : Context ,
uri : Annotated [ str , " URI of the script to validate under Assets/ directory, unity://path/Assets/... or file://... or Assets/... " ] ,
level : Annotated [ Literal [ ' basic ' , ' standard ' ] ,
" Validation level " ] = " basic " ,
include_diagnostics : Annotated [ bool ,
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
" Include full diagnostics and summary " ] = False ,
2025-10-25 12:53:53 +08:00
) - > dict [ str , Any ] :
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
unity_instance = get_unity_instance_from_context ( ctx )
ctx . info ( f " Processing validate_script: { uri } (unity_instance= { unity_instance or ' default ' } ) " )
2025-10-25 12:53:53 +08:00
name , directory = _split_uri ( uri )
if not directory or directory . split ( " / " ) [ 0 ] . lower ( ) != " assets " :
return { " success " : False , " code " : " path_outside_assets " , " message " : " URI must resolve under ' Assets/ ' . " }
if level not in ( " basic " , " standard " ) :
return { " success " : False , " code " : " bad_level " , " message " : " level must be ' basic ' or ' standard ' . " }
params = {
" action " : " validate " ,
" name " : name ,
" path " : directory ,
" level " : level ,
}
2025-11-06 04:08:59 +08:00
resp = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
params ,
)
2025-10-25 12:53:53 +08:00
if isinstance ( resp , dict ) and resp . get ( " success " ) :
diags = resp . get ( " data " , { } ) . get ( " diagnostics " , [ ] ) or [ ]
warnings = sum ( 1 for d in diags if str (
d . get ( " severity " , " " ) ) . lower ( ) == " warning " )
errors = sum ( 1 for d in diags if str (
d . get ( " severity " , " " ) ) . lower ( ) in ( " error " , " fatal " ) )
if include_diagnostics :
return { " success " : True , " data " : { " diagnostics " : diags , " summary " : { " warnings " : warnings , " errors " : errors } } }
return { " success " : True , " data " : { " warnings " : warnings , " errors " : errors } }
return resp if isinstance ( resp , dict ) else { " success " : False , " message " : str ( resp ) }
@mcp_for_unity_tool ( description = ( " Compatibility router for legacy script operations. Prefer apply_text_edits (ranges) or script_apply_edits (structured) for edits. " ) )
def manage_script (
ctx : Context ,
action : Annotated [ Literal [ ' create ' , ' read ' , ' delete ' ] , " Perform CRUD operations on C# scripts. " ] ,
name : Annotated [ str , " Script name (no .cs extension) " , " Name of the script to create " ] ,
path : Annotated [ str , " Asset path (default: ' Assets/ ' ) " , " Path under Assets/ to create the script at, e.g., ' Assets/Scripts/My.cs ' " ] ,
contents : Annotated [ str , " Contents of the script to create " ,
" C# code for ' create ' / ' update ' " ] | None = None ,
script_type : Annotated [ str , " Script type (e.g., ' C# ' ) " ,
" Type hint (e.g., ' MonoBehaviour ' ) " ] | None = None ,
namespace : Annotated [ str , " Namespace for the script " ] | None = None ,
) - > dict [ str , Any ] :
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
unity_instance = get_unity_instance_from_context ( ctx )
ctx . info ( f " Processing manage_script: { action } (unity_instance= { unity_instance or ' default ' } ) " )
2025-10-25 12:53:53 +08:00
try :
# Prepare parameters for Unity
params = {
" action " : action ,
" name " : name ,
" path " : path ,
" namespace " : namespace ,
" scriptType " : script_type ,
}
# Base64 encode the contents if they exist to avoid JSON escaping issues
if contents :
if action == ' create ' :
params [ " encodedContents " ] = base64 . b64encode (
contents . encode ( ' utf-8 ' ) ) . decode ( ' utf-8 ' )
params [ " contentsEncoded " ] = True
else :
params [ " contents " ] = contents
params = { k : v for k , v in params . items ( ) if v is not None }
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
response = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
params ,
)
2025-10-25 12:53:53 +08:00
if isinstance ( response , dict ) :
if response . get ( " success " ) :
if response . get ( " data " , { } ) . get ( " contentsEncoded " ) :
decoded_contents = base64 . b64decode (
response [ " data " ] [ " encodedContents " ] ) . decode ( ' utf-8 ' )
response [ " data " ] [ " contents " ] = decoded_contents
del response [ " data " ] [ " encodedContents " ]
del response [ " data " ] [ " contentsEncoded " ]
return {
" success " : True ,
" message " : response . get ( " message " , " Operation successful. " ) ,
" data " : response . get ( " data " ) ,
}
return response
return { " success " : False , " message " : str ( response ) }
except Exception as e :
return {
" success " : False ,
" message " : f " Python error managing script: { str ( e ) } " ,
}
@mcp_for_unity_tool ( description = (
""" Get manage_script capabilities (supported ops, limits, and guards).
Returns :
- ops : list of supported structured ops
- text_ops : list of supported text ops
- max_edit_payload_bytes : server edit payload cap
- guards : header / using guard enabled flag """
) )
def manage_script_capabilities ( ctx : Context ) - > dict [ str , Any ] :
ctx . info ( " Processing manage_script_capabilities " )
try :
# Keep in sync with server/Editor ManageScript implementation
ops = [
" replace_class " , " delete_class " , " replace_method " , " delete_method " ,
" insert_method " , " anchor_insert " , " anchor_delete " , " anchor_replace "
]
text_ops = [ " replace_range " , " regex_replace " , " prepend " , " append " ]
# Match ManageScript.MaxEditPayloadBytes if exposed; hardcode a sensible default fallback
max_edit_payload_bytes = 256 * 1024
guards = { " using_guard " : True }
extras = { " get_sha " : True }
return { " success " : True , " data " : {
" ops " : ops ,
" text_ops " : text_ops ,
" max_edit_payload_bytes " : max_edit_payload_bytes ,
" guards " : guards ,
" extras " : extras ,
} }
except Exception as e :
return { " success " : False , " error " : f " capabilities error: { e } " }
@mcp_for_unity_tool ( description = " Get SHA256 and basic metadata for a Unity C# script without returning file contents " )
def get_sha (
ctx : Context ,
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
uri : Annotated [ str , " URI of the script to edit under Assets/ directory, unity://path/Assets/... or file://... or Assets/... " ] ,
2025-10-25 12:53:53 +08:00
) - > dict [ str , Any ] :
Feature/session based instance routing (#369)
* Add support for multiple Unity instances
* fix port detection
* add missing unity_instance parameter
* add instance params for resources
* Fix CodeRabbit review feedback
- Fix partial framed response handling in port discovery
Add _recv_exact() helper to ensure complete frame reading
Prevents healthy Unity instances from being misidentified as offline
- Remove unused default_conn variables in server.py (2 files)
Fixes Ruff F841 lint error that would block CI/CD
- Preserve sync/async nature of resources in wrapper
Check if original function is coroutine before wrapping
Prevents 'dict object is not awaitable' runtime errors
- Fix reconnection to preserve instance_id
Add instance_id tracking to UnityConnection dataclass
Reconnection now targets the same Unity instance instead of any available one
Prevents operations from being applied to wrong project
- Add instance logging to manage_asset for debugging
Helps troubleshoot multi-instance scenarios
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix CodeRabbit feedback: reconnection fallback and annotations safety
Address 3 CodeRabbit review comments:
1. Critical: Guard reconnection fallback to prevent wrong instance routing
- When instance_id is set but rediscovery fails, now raises ConnectionError
- Added 'from e' to preserve exception chain for better debugging
- Prevents silently connecting to different Unity instance
- Ensures multi-instance routing integrity
2. Minor: Guard __annotations__ access in resource registration
- Use getattr(func, '__annotations__', {}) instead of direct access
- Prevents AttributeError for functions without type hints
3. Minor: Remove unused get_type_hints import
- Clean up unused import in resources/__init__.py
All changes applied to both Server/ and MCPForUnity/ directories.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix instance sorting and logging issues
- Fix sorting logic for instances without heartbeat data: use epoch timestamp instead of current time to properly deprioritize instances with None last_heartbeat
- Use logger.exception() instead of logger.error() in disconnect_all() to include stack traces for better debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* update uv.lock to prepare for merging into main
* Restore Python 3.10 lockfiles and package list_unity_instances tool
* Deduplicate Unity instance discovery by port
* Scope status-file reload checks to the active instance
* refactor: implement FastMCP middleware for session-based instance routing
Replaces module-level session_state.py with UnityInstanceMiddleware class
that follows FastMCP best practices. Middleware intercepts all tool calls
via on_call_tool hook and injects active Unity instance into request state.
Key changes:
- Add UnityInstanceMiddleware class with on_call_tool hook
- Tools now use ctx.get_state("unity_instance") instead of direct session_state calls
- Remove unity_instance parameter from all tool schemas to prevent LLM hallucination
- Convert list_unity_instances tool to unity_instances resource (read-only data)
- Update error messages to reference unity://instances resource
- Add set_state/get_state methods to DummyContext test helper
- All 67 tests passing (55 passed, 5 skipped, 7 xpassed)
Architecture benefits:
- Centralized session management in middleware
- Standard FastMCP patterns (middleware + request state)
- Cleaner separation of concerns
- Prevents AI hallucination of invalid instance IDs
* fix: convert resource templates to static resources for discoverability
Convert MCP resources from URI templates with query parameters to static
resources to fix discoverability in MCP clients like Claude Code.
Changes:
- Remove {?force_refresh} from unity://instances
- Remove {?unity_instance} from mcpforunity://menu-items
- Remove {?unity_instance} from mcpforunity://tests
- Keep {mode} path parameter in mcpforunity://tests/{mode} (legitimate)
Root cause: Query parameters {?param} trigger ResourceTemplate registration,
which are listed via resources/templates/list instead of resources/list.
Claude Code's ListMcpResourcesTool only queries resources/list, making
templates undiscoverable.
Solution: Remove optional query parameters from URIs. Instance routing is
handled by middleware/context, and force_refresh was cache control that
doesn't belong in resource identity.
Impact: Resources now discoverable via standard resources/list endpoint and
work with all MCP clients including Claude Code and Cursor.
Requires FastMCP >=2.13.0 for proper RFC 6570 query parameter support.
* feat: improve material properties and sync Server resources
Material Property Improvements (ManageAsset.cs):
- Add GetMainColorPropertyName() helper that auto-detects shader color properties
- Tries _BaseColor (URP), _Color (Standard), _MainColor, _Tint, _TintColor
- Update both named and array color property handling to use auto-detection
- Add warning messages when color properties don't exist on materials
- Split HasProperty check from SetColor to enable error reporting
This fixes the issue where simple color array format [r,g,b,a] defaulted to
_Color property, causing silent failures with URP Lit shader which uses _BaseColor.
Server Resource Sync:
- Sync Server/resources with MCPForUnity/UnityMcpServer~/src/resources
- Remove query parameters from resource URIs for discoverability
- Use session-based instance routing via get_unity_instance_from_context()
* 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
* refactor: standardize instance extraction and remove dead imports
- Standardize all 18 tools to use get_unity_instance_from_context() helper
instead of direct ctx.get_state() calls for consistency
- Remove dead session_state imports from with_unity_instance decorator
that would cause ModuleNotFoundError at runtime
- Update README.md with concise instance routing documentation
* fix: critical timezone and import bugs from code review
- Remove incorrect port safety check that treated reclaimed ports as errors
(GetPortWithFallback may legitimately return same port if it became available)
- Fix timezone-aware vs naive datetime mixing in unity_connection.py sorting
(use timestamp() for comparison to avoid TypeError)
- Normalize all datetime comparisons in port_discovery.py to UTC
(file_mtime and last_heartbeat now consistently timezone-aware)
- Add missing send_with_unity_instance import in Server/tools/manage_script.py
(was causing NameError at runtime on lines 108 and 488)
All 88 tests pass (76 passed + 5 skipped + 7 xpassed)
---------
Co-authored-by: Sakura <sakurachan@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:43:36 +08:00
unity_instance = get_unity_instance_from_context ( ctx )
ctx . info ( f " Processing get_sha: { uri } (unity_instance= { unity_instance or ' default ' } ) " )
2025-10-25 12:53:53 +08:00
try :
name , directory = _split_uri ( uri )
params = { " action " : " get_sha " , " name " : name , " path " : directory }
2025-11-06 04:08:59 +08:00
resp = send_with_unity_instance (
unity_connection . send_command_with_retry ,
unity_instance ,
" manage_script " ,
params ,
)
2025-10-25 12:53:53 +08:00
if isinstance ( resp , dict ) and resp . get ( " success " ) :
data = resp . get ( " data " , { } )
minimal = { " sha256 " : data . get (
" sha256 " ) , " lengthBytes " : data . get ( " lengthBytes " ) }
return { " success " : True , " data " : minimal }
return resp if isinstance ( resp , dict ) else { " success " : False , " message " : str ( resp ) }
except Exception as e :
return { " success " : False , " message " : f " get_sha error: { e } " }