Root cause
Injection & unsafe execution introduced
Fix UnicodeDecodeError in Docker for MCP Google Maps initialization
AI-assisted change: Claude
How AI contributed: Direct introductionPublished Apr 10, 2026 in
mervinpraison/praisonaiAll 19 findings in this repository
Sensitive environment variable exposure via MCP stdio subprocess (GHSA-PJ2R-F9MW-VRCQ / CVE-2026-40159): MCP.__init__ in src/praisonai-agents/praisonaiagents/mcp/mcp.py does env = kwargs.get('env', {}); if not env: env = os.environ.copy() and forwards the full parent environment to the spawned npx/subprocess stdio server, leaking API keys/tokens/DB credentials to untrusted third-party MCP commands.
Root cause
Fix UnicodeDecodeError in Docker for MCP Google Maps initialization
AI-assisted change: Claude
Fix
The fix stops copying the parent environment and builds each MCP subprocess environment from a small platform-safe baseline plus variables explicitly supplied by the caller.
AI-assisted fix: GitHub Copilot + Claude Code · praisonai-triage-agent[bot]
Case facts
@@ -40,12 +40,26 @@ def __init__(self, command: str = "npx", args: List[str] = None, timeout: int = logging.getLogger("npx-mcp-wrapper").setLevel(logging.DEBUG) os.environ["DEBUG"] = "mcp:*" + # Ensure UTF-8 encoding for Docker compatibility+ self._setup_utf8_environment()+ self._tools = [] self._function_declarations = [] # Initialize the MCP tools self._initialize_mcp_tools() + def _setup_utf8_environment(self):+ """+ Set up UTF-8 encoding environment variables for Docker compatibility.+ This prevents UnicodeDecodeError when using subprocess in Docker containers.+ """+ os.environ.update({+ 'PYTHONIOENCODING': 'utf-8',+ 'LC_ALL': 'C.UTF-8',+ 'LANG': 'C.UTF-8'+ })+ def _initialize_mcp_tools(self): """ Initialize the MCP tools by running a script that extracts the tool definitions.@@ -109,12 +123,12 @@ async def extract_tools(command, args): # Write the temporary script to a file temp_script_path = os.path.join(os.path.dirname(__file__), "_temp_extract_mcp_tools.py")- with open(temp_script_path, "w") as f:+ with open(temp_script_path, "w", encoding='utf-8') as f: f.write(temp_script) # Run the temporary script to extract the tool definitions cmd = ["python", temp_script_path, self.command] + self.args- result = subprocess.run(cmd, capture_output=True, text=True, timeout=self.timeout)+ result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', env=os.environ.copy(), timeout=self.timeout) # Remove the temporary script os.remove(temp_script_path)@@ -197,12 +211,12 @@ async def call_tool(command, args, tool_name, kwargs): try: # Write the temporary script to a file temp_script_path = os.path.join(os.path.dirname(__file__), f"_temp_call_mcp_tool_{tool_name}.py")- with open(temp_script_path, "w") as f:+ with open(temp_script_path, "w", encoding='utf-8') as f: f.write(temp_script) # Run the temporary script to call the MCP tool cmd = ["python", temp_script_path, self.command] + self.args + [tool_name, json.dumps(kwargs)]- result = subprocess.run(cmd, capture_output=True, text=True, timeout=self.timeout)+ result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', env=os.environ.copy(), timeout=self.timeout) # Remove the temporary script os.remove(temp_script_path)@@ -212,6 +212,18 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 # Set up stdio client self.is_sse = False+ + # Ensure UTF-8 encoding in environment for Docker compatibility+ env = kwargs.get('env', {})+ if not env:+ env = os.environ.copy()+ env.update({+ 'PYTHONIOENCODING': 'utf-8',+ 'LC_ALL': 'C.UTF-8',+ 'LANG': 'C.UTF-8'+ })+ kwargs['env'] = env+ self.server_params = StdioServerParameters( command=cmd, args=arguments,@@ -187,7 +187,8 @@ class MCP: ``` """ - def __init__(self, command_or_string=None, args=None, *, command=None, timeout=60, debug=False, **kwargs):+ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=60, debug=False, + allowed_tools: Optional[List[str]] = None, disabled_tools: Optional[List[str]] = None, **kwargs): """ Initialize the MCP connection and get tools. @@ -201,7 +202,13 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 command: Alternative parameter name for backward compatibility timeout: Timeout in seconds for MCP server initialization and tool calls (default: 60) debug: Enable debug logging for MCP operations (default: False)+ allowed_tools: Include whitelist - only these tools will be available (default: None = all tools)+ disabled_tools: Exclude blacklist - these tools will be filtered out (default: None = no exclusions) **kwargs: Additional parameters for StdioServerParameters+ + Note:+ If both allowed_tools and disabled_tools are specified, allowed_tools takes precedence+ (include wins over exclude). """ # Check if MCP is available if not MCP_AVAILABLE:@@ -240,6 +247,8 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 # Store additional parameters self.timeout = timeout self.debug = debug+ self.allowed_tools = allowed_tools+ self.disabled_tools = disabled_tools # Check if this is a WebSocket URL (ws:// or wss://) if isinstance(command_or_string, str) and re.match(r'^wss?://', command_or_string):@@ -254,7 +263,7 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 auth_token=auth_token, options=kwargs )- self._tools = list(self.websocket_client.tools)+ self._tools = self._apply_tool_filters(list(self.websocket_client.tools)) self.is_sse = False self.is_http_stream = False self.is_websocket = True@@ -268,7 +277,7 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 # Legacy SSE URL - use SSE transport for backward compatibility from .mcp_sse import SSEMCPClient self.sse_client = SSEMCPClient(command_or_string, debug=debug, timeout=timeout)- self._tools = list(self.sse_client.tools)+ self._tools = self._apply_tool_filters(list(self.sse_client.tools)) self.is_sse = True self.is_http_stream = False self.is_npx = False@@ -295,7 +304,7 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 timeout=timeout, options=transport_options )- self._tools = list(self.http_stream_client.tools)+ self._tools = self._apply_tool_filters(list(self.http_stream_client.tools)) self.is_sse = False self.is_http_stream = True self.is_npx = False@@ -325,20 +334,10 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 self.is_sse = False self.is_http_stream = False - # Ensure UTF-8 encoding in environment for Docker compatibility- env = kwargs.get('env', {})- if not env:- env = os.environ.copy()- - # Always set Python encoding- env['PYTHONIOENCODING'] = 'utf-8'- - # Only set locale variables on Unix systems- if platform.system() != 'Windows':- env.update({- 'LC_ALL': 'C.UTF-8',- 'LANG': 'C.UTF-8'- })+ # Build safe environment for stdio MCP servers+ # Use safe baseline + explicit env from config (B5 security policy)+ custom_env = kwargs.get('env', {})+ env = self._build_safe_env(custom_env) kwargs['env'] = env @@ -369,7 +368,7 @@ def __init__(self, command_or_string=None, args=None, *, command=None, timeout=6 self._initialize_npx_mcp_tools(cmd, arguments) else: # Generate tool functions immediately and store them- self._tools = self._generate_tool_functions()AI-assisted change f0194324e19c8c5a2154483671c9cdfaa5dabb159beaaa8066827327f26697ce · Fix 55f8d29c1b8b1cc254af70c8d37ea6a6486bb557399bde39695bdaff69f0db3c