From d18bf91875fad2baf9fc646c452a9526087dee3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=B5=E0=A4=BE=E0=A4=B8=E0=A5=81=E0=A4=95=E0=A4=BF=20?= =?UTF-8?q?=E0=A4=95=E0=A4=B6=E0=A5=8D=E0=A4=AF=E0=A4=AA?= <67409491+gajzzs@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:52:59 +0530 Subject: [PATCH 1/5] Change fastmcp dependency version to 2.12.5 https://github.com/CoplayDev/unity-mcp/issues/430 --- Server/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/pyproject.toml b/Server/pyproject.toml index c8138ba35..7bda0a7af 100644 --- a/Server/pyproject.toml +++ b/Server/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", - "fastmcp>=2.13.0,<2.13.2", + "fastmcp==2.12.5", "mcp>=1.16.0", "pydantic>=2.12.0", "tomli>=2.3.0", From b9c73658f04f9c3ee779289c20fb405f4b3ad8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=B5=E0=A4=BE=E0=A4=B8=E0=A5=81=E0=A4=95=E0=A4=BF=20?= =?UTF-8?q?=E0=A4=95=E0=A4=B6=E0=A5=8D=E0=A4=AF=E0=A4=AA?= <67409491+gajzzs@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:28:00 +0530 Subject: [PATCH 2/5] Update fastmcp dependency version constraint --- Server/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/pyproject.toml b/Server/pyproject.toml index 7bda0a7af..3b4e521b8 100644 --- a/Server/pyproject.toml +++ b/Server/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", - "fastmcp==2.12.5", + "fastmcp<2.13.0", "mcp>=1.16.0", "pydantic>=2.12.0", "tomli>=2.3.0", From aa81ee671cbefd6ed988061b23e3a2bf21f684d9 Mon Sep 17 00:00:00 2001 From: Adarsh Raj <67409491+vi0sapio@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:28:11 +0530 Subject: [PATCH 3/5] Fix issue https://github.com/CoplayDev/unity-mcp/issues/430 --- Server/pyproject.toml | 2 +- Server/src/main.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Server/pyproject.toml b/Server/pyproject.toml index 3b4e521b8..c8138ba35 100644 --- a/Server/pyproject.toml +++ b/Server/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "httpx>=0.27.2", - "fastmcp<2.13.0", + "fastmcp>=2.13.0,<2.13.2", "mcp>=1.16.0", "pydantic>=2.12.0", "tomli>=2.3.0", diff --git a/Server/src/main.py b/Server/src/main.py index ee04d5351..01b31a09d 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -3,11 +3,50 @@ import logging from contextlib import asynccontextmanager import os +import sys import threading import time from typing import AsyncIterator, Any from urllib.parse import urlparse +if sys.platform == 'win32': + import msvcrt + import io + + # Set binary mode on stdin/stdout to prevent automatic translation at the FD level + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + + # Define a proxy buffer that actively strips \r bytes from the output stream + class CRStripper: + def __init__(self, stream): + self._stream = stream + + def write(self, data): + if isinstance(data, bytes): + return self._stream.write(data.replace(b'\r', b'')) + if isinstance(data, str): + return self._stream.write(data.replace('\r', '')) + return self._stream.write(data) + + def flush(self): + return self._stream.flush() + + def __getattr__(self, name): + return getattr(self._stream, name) + + # Detach the underlying buffer from the current sys.stdout + # and re-wrap it with our CRStripper and a new TextIOWrapper + _original_buffer = sys.stdout.detach() + sys.stdout = io.TextIOWrapper( + CRStripper(_original_buffer), + encoding=sys.stdout.encoding or 'utf-8', + errors=sys.stdout.errors or 'strict', + newline='\n', # Enforce LF for text writing + line_buffering=True, + write_through=True + ) + from fastmcp import FastMCP from logging.handlers import RotatingFileHandler from starlette.requests import Request From 0f89329114000bd12b0e9dda5591871ed5234d5e Mon Sep 17 00:00:00 2001 From: Adarsh Raj <67409491+vi0sapio@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:46:37 +0530 Subject: [PATCH 4/5] Changes https://github.com/CoplayDev/unity-mcp/pull/457\#issuecomment-3657356387 --- Server/src/main.py | 50 +++++++++------------------------ Server/src/utils/__init__.py | 6 ++++ Server/src/utils/cr_stripper.py | 18 ++++++++++++ 3 files changed, 38 insertions(+), 36 deletions(-) create mode 100644 Server/src/utils/__init__.py create mode 100644 Server/src/utils/cr_stripper.py diff --git a/Server/src/main.py b/Server/src/main.py index 01b31a09d..d379b55f3 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -1,51 +1,21 @@ import argparse import asyncio import logging -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, redirect_stdout import os import sys import threading import time from typing import AsyncIterator, Any from urllib.parse import urlparse +import io +from utils.cr_stripper import CRStripper if sys.platform == 'win32': import msvcrt - import io - # Set binary mode on stdin/stdout to prevent automatic translation at the FD level - msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - - # Define a proxy buffer that actively strips \r bytes from the output stream - class CRStripper: - def __init__(self, stream): - self._stream = stream - - def write(self, data): - if isinstance(data, bytes): - return self._stream.write(data.replace(b'\r', b'')) - if isinstance(data, str): - return self._stream.write(data.replace('\r', '')) - return self._stream.write(data) - - def flush(self): - return self._stream.flush() - - def __getattr__(self, name): - return getattr(self._stream, name) - - # Detach the underlying buffer from the current sys.stdout - # and re-wrap it with our CRStripper and a new TextIOWrapper - _original_buffer = sys.stdout.detach() - sys.stdout = io.TextIOWrapper( - CRStripper(_original_buffer), - encoding=sys.stdout.encoding or 'utf-8', - errors=sys.stdout.errors or 'strict', - newline='\n', # Enforce LF for text writing - line_buffering=True, - write_through=True - ) + from fastmcp import FastMCP from logging.handlers import RotatingFileHandler @@ -441,8 +411,16 @@ def main(): mcp.run(transport=transport, host=host, port=port) else: # Use stdio transport for traditional MCP - logger.info("Starting FastMCP with stdio transport") - mcp.run(transport='stdio') + removed_crlf = io.TextIOWrapper( + CRStripper(sys.stdout.buffer), + encoding=sys.stdout.encoding or 'utf-8', + newline='\n', + line_buffering=True, + ) + + with redirect_stdout(removed_crlf): + logger.info("Starting FastMCP with stdio transport") + mcp.run(transport='stdio') # Run the server diff --git a/Server/src/utils/__init__.py b/Server/src/utils/__init__.py new file mode 100644 index 000000000..f15121127 --- /dev/null +++ b/Server/src/utils/__init__.py @@ -0,0 +1,6 @@ +""" +SUMMARY: Utils package initialization. +""" +from .cr_stripper import CRStripper + +__all__ = ["CRStripper"] diff --git a/Server/src/utils/cr_stripper.py b/Server/src/utils/cr_stripper.py new file mode 100644 index 000000000..a4249cc0a --- /dev/null +++ b/Server/src/utils/cr_stripper.py @@ -0,0 +1,18 @@ + +# This utility strips carriage return characters (\r) from bytes or strings +class CRStripper: + def __init__(self, stream): + self._stream = stream + + def write(self, data): + if isinstance(data, bytes): + return self._stream.write(data.replace(b'\r', b'')) + if isinstance(data, str): + return self._stream.write(data.replace('\r', '')) + return self._stream.write(data) + + def flush(self): + return self._stream.flush() + + def __getattr__(self, name): + return getattr(self._stream, name) From 2c09a781b27a0bb719436d9cdfc0b85a3fab85f4 Mon Sep 17 00:00:00 2001 From: Adarsh Raj <67409491+vi0sapio@users.noreply.github.com> Date: Sat, 20 Dec 2025 08:44:10 +0530 Subject: [PATCH 5/5] docs(utils): enhance CRStripper docstrings and type hinting - Fix Return value to reflect input length after character removal. --- Server/src/main.py | 2 +- Server/src/utils/cr_stripper.py | 77 ++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/Server/src/main.py b/Server/src/main.py index d379b55f3..11a033e51 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -13,7 +13,7 @@ if sys.platform == 'win32': import msvcrt - # Set binary mode on stdin/stdout to prevent automatic translation at the FD level + # Set binary mode on stdout to prevent automatic translation at the FD level msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) diff --git a/Server/src/utils/cr_stripper.py b/Server/src/utils/cr_stripper.py index a4249cc0a..c6dc6bfb2 100644 --- a/Server/src/utils/cr_stripper.py +++ b/Server/src/utils/cr_stripper.py @@ -1,18 +1,75 @@ +""" +Provides a utility to strip carriage return characters from output streams. + +This module implements the `CRStripper` class, which wraps a file-like object +to filter out carriage return (\r) characters during write operations. + +Usage of this wrapper is essential for Model Context Protocol (MCP) communication +over stdio, as it ensures consistent line endings and safeguards against +protocol errors, particularly in Windows environments. +""" + +from typing import Any, BinaryIO -# This utility strips carriage return characters (\r) from bytes or strings class CRStripper: - def __init__(self, stream): + """ + A file-like wrapper that strips carriage return (\r) characters from data before writing. + + This class intercepts write calls to the underlying stream and removes all + instances of '\r', ensuring that output is clean and consistent across + different platforms. + """ + def __init__(self, stream: BinaryIO) -> None: + """ + Initialize the stripper with an underlying stream. + + Args: + stream (BinaryIO): The underlying file-like object or buffer to wrap (e.g., sys.stdout.buffer). + """ self._stream = stream - def write(self, data): - if isinstance(data, bytes): - return self._stream.write(data.replace(b'\r', b'')) - if isinstance(data, str): - return self._stream.write(data.replace('\r', '')) - return self._stream.write(data) + def write(self, data: bytes | bytearray | str) -> int: + """ + Write data to the underlying stream after stripping all carriage return characters. + + Args: + data (bytes | bytearray | str): The data to be written. + + Returns: + int: The number of bytes or characters processed (matches input length if successful). + """ + if isinstance(data, (bytes, bytearray)): + stripped = data.replace(b'\r', b'') + written = self._stream.write(stripped) + elif isinstance(data, str): + stripped = data.replace('\r', '') + written = self._stream.write(stripped) + else: + return self._stream.write(data) + + # If the underlying stream wrote all the stripped data, we report + # that we wrote all the ORIGINAL data. + # This prevents callers (like TextIOWrapper) from seeing a "partial write" + # mismatch when we intentionally removed characters. + if written == len(stripped): + return len(data) + + return written - def flush(self): + def flush(self) -> None: + """ + Flush the underlying stream. + """ return self._stream.flush() - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: + """ + Delegate any attribute or method access to the underlying stream. + + Args: + name (str): The name of the attribute to access. + + Returns: + Any: The attribute or method from the wrapped stream. + """ return getattr(self._stream, name)