From 812bc0297846a917a6880b0a7a59328c0189ceec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arribas=20Lopera?= Date: Sun, 28 Dec 2025 11:52:51 +0100 Subject: [PATCH 1/2] fix: resolve Windows UTF-8 encoding issue for international characters Fixes issue #1 where UTF-8 characters (Chinese, etc.) were corrupted when passed through MCP on Windows systems. Changes: - Use explicit UTF-8 encoding for subprocess I/O on Windows instead of system default code page (cp1252, cp936, etc.) - Set PYTHONUTF8=1 and PYTHONIOENCODING=utf-8 environment variables - Encode input as UTF-8 bytes and decode output with error handling - Add .ps1 to Windows executable extensions - Add fallback checks for common Windows installation paths - Improve error diagnostics with platform-specific messages - Add FileNotFoundError handling with actionable guidance Tested: - Python syntax validation passes - All version numbers updated to 1.2.3 - Changelog updated with v1.2.2 and v1.2.3 entries Resolves: #1 --- CHANGELOG.md | 37 +++++++++++++ pyproject.toml | 2 +- src/__init__.py | 4 +- src/mcp_server.py | 134 ++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 157 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fef4184..011c437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.3] - 2025-12-28 + +### Fixed +- **Windows UTF-8 Encoding**: Fixed international character corruption on Windows systems + - Subprocess I/O now explicitly uses UTF-8 encoding instead of system default code page + - Sets `PYTHONUTF8=1` and `PYTHONIOENCODING=utf-8` environment variables for Windows + - Properly encodes input as UTF-8 bytes and decodes output with error handling + - Resolves issue #1 where Chinese and other non-ASCII characters were corrupted + +### Added +- **Enhanced Windows CLI Detection**: Improved Codex CLI path detection on Windows + - Added `.ps1` to supported Windows executable extensions + - Added fallback checks for common Windows installation paths: + - `%LOCALAPPDATA%\Programs\codex\codex.exe` + - `%APPDATA%\npm\codex.cmd` + - `%USERPROFILE%\.cargo\bin\codex.exe` +- **Improved Error Diagnostics**: Better error messages for Windows users + - Added `FileNotFoundError` specific handling with actionable guidance + - Error responses now include platform information for debugging + - Clear instructions to verify `codex --version` in Command Prompt + +### Changed +- **Error Metadata**: All error responses now include `platform` and `exception_type` fields +- **Documentation**: Updated module docstring to mention Windows UTF-8 compatibility + +## [1.2.2] - 2025-09-09 + +### Fixed +- **Windows Compatibility**: Added platform-specific subprocess handling for Windows + - Removed `start_new_session=True` which is not supported on Windows + - Added Windows executable detection (.exe, .bat, .cmd extensions) + - Created `_run_codex_command()` helper for cross-platform execution + +### Added +- **Platform Detection Utilities**: `_is_windows()` and `_get_codex_command()` functions +- **CI/CD Improvements**: Updated GitHub Actions for PyPI Trusted Publishing + ## [1.2.1] - 2025-09-04 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 5210530..ce146e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "codex-bridge" -version = "1.2.2" +version = "1.2.3" description = "Lightweight MCP server bridging Claude Code to OpenAI Codex via official CLI" readme = "README.md" license = "MIT" diff --git a/src/__init__.py b/src/__init__.py index fa3fabb..19c9766 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,9 +1,9 @@ """ MCP Codex Assistant - Simple CLI bridge to OpenAI Codex. -Version 1.2.2 - Windows compatibility fix for cross-platform support. +Version 1.2.3 - Windows UTF-8 encoding fix for international character support. """ from .mcp_server import main -__version__ = "1.2.2" +__version__ = "1.2.3" __all__ = ["main"] \ No newline at end of file diff --git a/src/mcp_server.py b/src/mcp_server.py index cdbabaf..b962186 100644 --- a/src/mcp_server.py +++ b/src/mcp_server.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Codex MCP Server - Simple CLI Bridge -Version 1.2.1 +Version 1.2.3 A minimal MCP server to interface with OpenAI Codex via the codex CLI. Created by @shelakh/elyin @@ -10,6 +10,9 @@ This server does ONE thing: bridge Claude to Codex CLI. Nothing more. Non-interactive execution with JSON output and batch processing support. + +Windows compatibility: Uses UTF-8 encoding for subprocess I/O to handle +international characters correctly on Windows systems. """ import json @@ -33,7 +36,7 @@ def _is_windows() -> bool: def _get_codex_command() -> Optional[str]: """Get the codex command path with Windows compatibility. - + Returns: Path to codex executable or None if not found """ @@ -41,14 +44,24 @@ def _get_codex_command() -> Optional[str]: codex_path = shutil.which("codex") if codex_path: return codex_path - + # On Windows, explicitly check for common executable extensions if _is_windows(): - for ext in ['.exe', '.bat', '.cmd']: + for ext in ['.exe', '.bat', '.cmd', '.ps1']: codex_path = shutil.which(f"codex{ext}") if codex_path: return codex_path - + + # Also check common installation paths on Windows + common_paths = [ + os.path.expandvars(r'%LOCALAPPDATA%\Programs\codex\codex.exe'), + os.path.expandvars(r'%APPDATA%\npm\codex.cmd'), + os.path.expandvars(r'%USERPROFILE%\.cargo\bin\codex.exe'), + ] + for path in common_paths: + if os.path.isfile(path): + return path + return None @@ -75,27 +88,45 @@ def _should_skip_git_check() -> bool: def _run_codex_command(cmd: List[str], directory: str, timeout_value: int, input_text: str) -> subprocess.CompletedProcess: """Execute codex command with platform-specific handling. - + Args: cmd: Command list to execute directory: Working directory timeout_value: Timeout in seconds input_text: Input text to pipe to the command - + Returns: - CompletedProcess result + CompletedProcess result with stdout/stderr as strings """ - # Windows-specific handling + # Windows-specific handling with UTF-8 encoding support if _is_windows(): - # On Windows, don't use start_new_session as it's not supported - return subprocess.run( + # On Windows, we need to: + # 1. Use encoding='utf-8' instead of text=True to avoid code page issues + # 2. Set PYTHONUTF8=1 and PYTHONIOENCODING=utf-8 for consistent encoding + # 3. Don't use start_new_session as it's not supported on Windows + env = os.environ.copy() + env['PYTHONUTF8'] = '1' + env['PYTHONIOENCODING'] = 'utf-8' + + # Encode input as UTF-8 bytes + input_bytes = input_text.encode('utf-8') if input_text else None + + result = subprocess.run( cmd, cwd=directory, capture_output=True, - text=True, timeout=timeout_value, - input=input_text, - shell=False + input=input_bytes, + shell=False, + env=env + ) + + # Decode output as UTF-8 with error handling + return subprocess.CompletedProcess( + args=result.args, + returncode=result.returncode, + stdout=result.stdout.decode('utf-8', errors='replace') if result.stdout else '', + stderr=result.stderr.decode('utf-8', errors='replace') if result.stderr else '' ) else: # Unix/macOS handling (original behavior) @@ -328,15 +359,40 @@ def consult_codex( } }, indent=2) return error_response + except FileNotFoundError as e: + # More specific error for when codex command is not found + codex_path = _get_codex_command() + if _is_windows(): + error_response = ( + f"Error: Codex CLI not found or not executable. " + f"Detected path: {codex_path or 'None'}. " + f"Please ensure 'codex' is installed and in your PATH. " + f"Try running 'codex --version' in Command Prompt to verify." + ) + else: + error_response = f"Error: Codex CLI not found: {str(e)}" + if format == "json": + return json.dumps({ + "status": "error", + "error": error_response, + "metadata": { + "directory": directory, + "format": format, + "platform": platform.system() + } + }, indent=2) + return error_response except Exception as e: error_response = f"Error executing Codex CLI: {str(e)}" if format == "json": return json.dumps({ - "status": "error", + "status": "error", "error": error_response, "metadata": { "directory": directory, - "format": format + "format": format, + "platform": platform.system(), + "exception_type": type(e).__name__ } }, indent=2) return error_response @@ -438,6 +494,29 @@ def consult_codex_with_stdin( } }, indent=2) return error_response + except FileNotFoundError as e: + # More specific error for when codex command is not found + codex_path = _get_codex_command() + if _is_windows(): + error_response = ( + f"Error: Codex CLI not found or not executable. " + f"Detected path: {codex_path or 'None'}. " + f"Please ensure 'codex' is installed and in your PATH. " + f"Try running 'codex --version' in Command Prompt to verify." + ) + else: + error_response = f"Error: Codex CLI not found: {str(e)}" + if format == "json": + return json.dumps({ + "status": "error", + "error": error_response, + "metadata": { + "directory": directory, + "format": format, + "platform": platform.system() + } + }, indent=2) + return error_response except Exception as e: error_response = f"Error executing Codex CLI: {str(e)}" if format == "json": @@ -446,7 +525,9 @@ def consult_codex_with_stdin( "error": error_response, "metadata": { "directory": directory, - "format": format + "format": format, + "platform": platform.system(), + "exception_type": type(e).__name__ } }, indent=2) return error_response @@ -564,6 +645,25 @@ def consult_codex_batch( "timeout": query_timeout } }) + except FileNotFoundError as e: + codex_path = _get_codex_command() + if _is_windows(): + error_msg = ( + f"Codex CLI not found or not executable. " + f"Detected path: {codex_path or 'None'}. " + f"Please ensure 'codex' is installed and in your PATH." + ) + else: + error_msg = f"Codex CLI not found: {str(e)}" + results.append({ + "status": "error", + "index": i, + "query": query[:100] + "..." if len(query) > 100 else query, + "error": error_msg, + "metadata": { + "platform": platform.system() + } + }) except Exception as e: results.append({ "status": "error", From 1e764643d5d2eb3fdedf7e5d6203b25020886025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Arribas=20Lopera?= Date: Sun, 28 Dec 2025 12:01:15 +0100 Subject: [PATCH 2/2] fix: use detected codex path in subprocess execution Addresses PR review feedback: the detected codex path (.ps1, .exe, etc.) was not being used in the actual subprocess execution - it was only used for the pre-flight check. Changes: - Add _build_codex_exec_command() helper that returns the proper command list based on the detected executable type - PowerShell scripts (.ps1) are now executed via: powershell -ExecutionPolicy Bypass -File exec - Windows .exe/.bat/.cmd files use the resolved absolute path - All three MCP tools now use _build_codex_exec_command() instead of hardcoded ["codex", "exec"] - Updated CHANGELOG with the new helper function This ensures that when Codex is installed as a PowerShell script, it will actually be executed correctly instead of failing with FileNotFoundError. --- CHANGELOG.md | 2 ++ src/mcp_server.py | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 011c437..bbee688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `%LOCALAPPDATA%\Programs\codex\codex.exe` - `%APPDATA%\npm\codex.cmd` - `%USERPROFILE%\.cargo\bin\codex.exe` + - New `_build_codex_exec_command()` helper that uses the detected path + - PowerShell scripts (.ps1) are now executed via `powershell -ExecutionPolicy Bypass -File` - **Improved Error Diagnostics**: Better error messages for Windows users - Added `FileNotFoundError` specific handling with actionable guidance - Error responses now include platform information for debugging diff --git a/src/mcp_server.py b/src/mcp_server.py index b962186..70aac83 100644 --- a/src/mcp_server.py +++ b/src/mcp_server.py @@ -65,6 +65,31 @@ def _get_codex_command() -> Optional[str]: return None +def _build_codex_exec_command() -> List[str]: + """Build the command list to execute codex exec. + + On Windows, if the codex CLI is a PowerShell script (.ps1), we need to + invoke it through PowerShell explicitly. Otherwise, use the resolved path + or fall back to 'codex' for PATH resolution. + + Returns: + Command list suitable for subprocess execution + """ + codex_path = _get_codex_command() + + if codex_path and _is_windows(): + # Check if it's a PowerShell script + if codex_path.lower().endswith('.ps1'): + # Execute PowerShell script: powershell -ExecutionPolicy Bypass -File script.ps1 exec + return ['powershell', '-ExecutionPolicy', 'Bypass', '-File', codex_path, 'exec'] + else: + # Use the resolved path directly for .exe, .bat, .cmd + return [codex_path, 'exec'] + + # For Unix or when codex is in PATH, use simple command + return ['codex', 'exec'] + + def _get_timeout() -> int: """Get timeout from environment variable or default to 90 seconds. @@ -317,17 +342,17 @@ def consult_codex( processed_query = query # Setup command and timeout - cmd = ["codex", "exec"] + cmd = _build_codex_exec_command() if _should_skip_git_check(): cmd.append("--skip-git-repo-check") timeout_value = timeout or _get_timeout() - + # Execute with timing start_time = time.time() try: result = _run_codex_command(cmd, directory, timeout_value, processed_query) execution_time = time.time() - start_time - + if result.returncode == 0: cleaned_output = _clean_codex_output(result.stdout) raw_response = cleaned_output if cleaned_output else "No output from Codex CLI" @@ -345,7 +370,7 @@ def consult_codex( } }, indent=2) return error_response - + except subprocess.TimeoutExpired: error_response = f"Error: Codex CLI command timed out after {timeout_value} seconds" if format == "json": @@ -444,15 +469,15 @@ def consult_codex_with_stdin( # Combine stdin content with prompt combined_input = f"{stdin_content}\n\n{prompt}" - + # Prepare query based on format if format == "json": processed_query = _format_prompt_for_json(combined_input) else: processed_query = combined_input - + # Setup command and timeout - cmd = ["codex", "exec"] + cmd = _build_codex_exec_command() if _should_skip_git_check(): cmd.append("--skip-git-repo-check") timeout_value = timeout or _get_timeout() @@ -597,7 +622,7 @@ def consult_codex_batch( # Process individual query processed_query = _format_prompt_for_json(query) - cmd = ["codex", "exec"] + cmd = _build_codex_exec_command() if _should_skip_git_check(): cmd.append("--skip-git-repo-check")