11import base64
22import os
33from typing import Annotated , Any , Literal
4- from urllib . parse import urlparse , unquote
4+
55
66from fastmcp import FastMCP , Context
77
1111import transport .legacy .unity_connection
1212
1313
14- def _split_uri (uri : str ) -> tuple [str , str ]:
15- """Split an incoming URI or path into (name, directory) suitable for Unity.
16-
17- Rules:
18- - unity://path/Assets/... → keep as Assets-relative (after decode/normalize)
19- - file://... → percent-decode, normalize, strip host and leading slashes,
20- then, if any 'Assets' segment exists, return path relative to that 'Assets' root.
21- Otherwise, fall back to original name/dir behavior.
22- - plain paths → decode/normalize separators; if they contain an 'Assets' segment,
23- return relative to 'Assets'.
24- """
25- raw_path : str
26- if uri .startswith ("unity://path/" ):
27- raw_path = uri [len ("unity://path/" ):]
28- elif uri .startswith ("file://" ):
29- parsed = urlparse (uri )
30- host = (parsed .netloc or "" ).strip ()
31- p = parsed .path or ""
32- # UNC: file://server/share/... -> //server/share/...
33- if host and host .lower () != "localhost" :
34- p = f"//{ host } { p } "
35- # Use percent-decoded path, preserving leading slashes
36- raw_path = unquote (p )
37- else :
38- raw_path = uri
39-
40- # Percent-decode any residual encodings and normalize separators
41- raw_path = unquote (raw_path ).replace ("\\ " , "/" )
42- # Strip leading slash only for Windows drive-letter forms like "/C:/..."
43- if os .name == "nt" and len (raw_path ) >= 3 and raw_path [0 ] == "/" and raw_path [2 ] == ":" :
44- raw_path = raw_path [1 :]
45-
46- # Normalize path (collapse ../, ./)
47- norm = os .path .normpath (raw_path ).replace ("\\ " , "/" )
48-
49- # If an 'Assets' segment exists, compute path relative to it (case-insensitive)
50- parts = [p for p in norm .split ("/" ) if p not in ("" , "." )]
51- idx = next ((i for i , seg in enumerate (parts )
52- if seg .lower () == "assets" ), None )
53- assets_rel = "/" .join (parts [idx :]) if idx is not None else None
54-
55- effective_path = assets_rel if assets_rel else norm
56- # For POSIX absolute paths outside Assets, drop the leading '/'
57- # to return a clean relative-like directory (e.g., '/tmp' -> 'tmp').
58- if effective_path .startswith ("/" ):
59- effective_path = effective_path [1 :]
60-
61- name = os .path .splitext (os .path .basename (effective_path ))[0 ]
62- directory = os .path .dirname (effective_path )
63- return name , directory
14+ from services .tools .utils import split_uri
6415
6516
6617@mcp_for_unity_tool (description = (
@@ -91,7 +42,7 @@ async def apply_text_edits(
9142 unity_instance = get_unity_instance_from_context (ctx )
9243 await ctx .info (
9344 f"Processing apply_text_edits: { uri } (unity_instance={ unity_instance or 'default' } )" )
94- name , directory = _split_uri (uri )
45+ name , directory = split_uri (uri )
9546
9647 # Normalize common aliases/misuses for resilience:
9748 # - Accept LSP-style range objects: {range:{start:{line,character}, end:{...}}, newText|text}
@@ -421,7 +372,7 @@ async def delete_script(
421372 unity_instance = get_unity_instance_from_context (ctx )
422373 await ctx .info (
423374 f"Processing delete_script: { uri } (unity_instance={ unity_instance or 'default' } )" )
424- name , directory = _split_uri (uri )
375+ name , directory = split_uri (uri )
425376 if not directory or directory .split ("/" )[0 ].lower () != "assets" :
426377 return {"success" : False , "code" : "path_outside_assets" , "message" : "URI must resolve under 'Assets/'." }
427378 params = {"action" : "delete" , "name" : name , "path" : directory }
@@ -446,7 +397,7 @@ async def validate_script(
446397 unity_instance = get_unity_instance_from_context (ctx )
447398 await ctx .info (
448399 f"Processing validate_script: { uri } (unity_instance={ unity_instance or 'default' } )" )
449- name , directory = _split_uri (uri )
400+ name , directory = split_uri (uri )
450401 if not directory or directory .split ("/" )[0 ].lower () != "assets" :
451402 return {"success" : False , "code" : "path_outside_assets" , "message" : "URI must resolve under 'Assets/'." }
452403 if level not in ("basic" , "standard" ):
@@ -584,7 +535,7 @@ async def get_sha(
584535 await ctx .info (
585536 f"Processing get_sha: { uri } (unity_instance={ unity_instance or 'default' } )" )
586537 try :
587- name , directory = _split_uri (uri )
538+ name , directory = split_uri (uri )
588539 params = {"action" : "get_sha" , "name" : name , "path" : directory }
589540 resp = await send_with_unity_instance (
590541 transport .legacy .unity_connection .async_send_command_with_retry ,
0 commit comments