diff --git a/Server/src/main.py b/Server/src/main.py index ee04d5351..11a033e51 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -1,12 +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 + # Set binary mode on stdout to prevent automatic translation at the FD level + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + from fastmcp import FastMCP from logging.handlers import RotatingFileHandler @@ -402,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..c6dc6bfb2 --- /dev/null +++ b/Server/src/utils/cr_stripper.py @@ -0,0 +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 + +class CRStripper: + """ + 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: 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) -> None: + """ + Flush the underlying stream. + """ + return self._stream.flush() + + 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)