Skip to content

Commit 8b327cb

Browse files
committed
Refactor Python server architecture and stability / Python 서버 아키텍처 및 안정성 리팩토링
1 parent 8a76ca1 commit 8b327cb

File tree

8 files changed

+473
-507
lines changed

8 files changed

+473
-507
lines changed

MCPForUnity/Server~/mcp_wrapper.py

Lines changed: 0 additions & 121 deletions
This file was deleted.

MCPForUnity/Server~/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ dev = [
2020
]
2121

2222
[project.scripts]
23-
mcp-for-unity = "main:main"
23+
mcp-for-unity = "src.main:main"
24+
25+
[tool.setuptools.packages.find]
26+
where = ["src"]
2427

2528
[build-system]
2629
requires = ["setuptools>=80.9.0", "wheel>=0.45.1"]

MCPForUnity/Server~/src/main.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,6 @@
6565
"httpx",
6666
"urllib3",
6767
"mcp.server.lowlevel.server",
68-
"uvicorn",
69-
"uvicorn.access",
70-
"uvicorn.error",
71-
"docket",
72-
"docket.worker",
73-
"fastmcp", # <--- 🚨 범인 검거
74-
"starlette" # <--- 혹시 모를 공범
7568
):
7669
try:
7770
logging.getLogger(noisy).setLevel(
@@ -412,9 +405,26 @@ def main():
412405
logger.info(f"Starting FastMCP with HTTP transport on {host}:{port}")
413406
mcp.run(transport=transport, host=host, port=port)
414407
else:
408+
415409
# Use stdio transport for traditional MCP
416410
logger.info("Starting FastMCP with stdio transport")
417-
mcp.run(transport='stdio', show_banner=False)
411+
# 🚨 [CRITICAL] In STDIO mode only, suppress related loggers.
412+
# Silence Uvicorn and related loggers to prevent stdout pollution.
413+
# 🚨 [CRITICAL] Prevent stdout pollution in STDIO mode
414+
for name in (
415+
"uvicorn", "uvicorn.error", "uvicorn.access",
416+
"starlette",
417+
"docket", "docket.worker",
418+
"fastmcp",
419+
):
420+
lg = logging.getLogger(name)
421+
lg.setLevel(logging.WARNING) # ERROR if still too chatty
422+
lg.propagate = False # prevent duplicate root logs
423+
# If the rotating file handler was successfully created, attach it
424+
if '_fh' in globals() and _fh not in lg.handlers:
425+
lg.addHandler(_fh)
426+
427+
mcp.run(transport='stdio')
418428

419429

420430
# Run the server

MCPForUnity/Server~/src/services/tools/manage_script.py

Lines changed: 6 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import base64
22
import os
33
from typing import Annotated, Any, Literal
4-
from urllib.parse import urlparse, unquote
4+
55

66
from fastmcp import FastMCP, Context
77

@@ -11,56 +11,7 @@
1111
import 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

Comments
 (0)