Skip to content

Commit 030379a

Browse files
CopilotOEvortex
andcommitted
Fix MCP server connection issue with graceful error handling
Co-authored-by: OEvortex <158988478+OEvortex@users.noreply.github.com>
1 parent 99d6123 commit 030379a

File tree

4 files changed

+245
-85
lines changed

4 files changed

+245
-85
lines changed

HelpingAI/client.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,12 +573,49 @@ def _convert_tools_parameter(
573573
f"Supported formats: None, string (category), List[Dict] (OpenAI format), "
574574
f"List[str] (built-in tools), or List[Fn]. Using legacy behavior."
575575
)
576+
elif "Failed to initialize MCP tools" in error_msg:
577+
# Handle MCP-specific errors with helpful guidance
578+
if "uvx" in error_msg:
579+
warnings.warn(
580+
f"Tool conversion failed: {e}. "
581+
f"Install uvx with: pip install uvx. Using legacy behavior."
582+
)
583+
elif "npx" in error_msg:
584+
warnings.warn(
585+
f"Tool conversion failed: {e}. "
586+
f"Install Node.js and npm to use npx commands. Using legacy behavior."
587+
)
588+
elif "fileno" in error_msg:
589+
warnings.warn(
590+
f"Tool conversion failed: {e}. "
591+
f"This may be due to a subprocess issue. Check MCP server configuration. Using legacy behavior."
592+
)
593+
else:
594+
warnings.warn(f"Tool conversion failed: {e}. Using legacy behavior.")
576595
else:
577596
warnings.warn(f"Tool conversion failed: {e}. Using legacy behavior.")
578597

579-
# Fallback to legacy behavior - return tools as-is if it's a list
598+
# Fallback to legacy behavior - filter out problematic items
580599
if isinstance(tools, list):
581-
return tools
600+
# Filter out MCP server configs and other problematic items
601+
filtered_tools = []
602+
for item in tools:
603+
if isinstance(item, str):
604+
# Keep string tools (built-in tool names) but warn
605+
filtered_tools.append({
606+
"type": "function",
607+
"function": {
608+
"name": item,
609+
"description": f"Built-in tool: {item}",
610+
"parameters": {"type": "object", "properties": {}, "required": []}
611+
}
612+
})
613+
elif isinstance(item, dict) and "type" in item and item.get("type") == "function":
614+
# Keep valid OpenAI format tools
615+
filtered_tools.append(item)
616+
# Skip MCP server configs and other problematic items
617+
618+
return filtered_tools if filtered_tools else None
582619
return None
583620

584621
def execute_tool_calls(

HelpingAI/tools/compatibility.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ def _handle_mcp_servers_config(mcp_config: Dict[str, Any]) -> List[Dict[str, Any
400400
401401
Raises:
402402
ImportError: If MCP dependencies are not available
403-
ValueError: If MCP configuration is invalid
403+
ValueError: If MCP configuration is invalid or all servers fail to connect
404404
"""
405405
try:
406406
from .mcp_manager import MCPManager
@@ -412,10 +412,43 @@ def _handle_mcp_servers_config(mcp_config: Dict[str, Any]) -> List[Dict[str, Any
412412

413413
# Initialize MCP manager and get tools
414414
manager = MCPManager()
415-
mcp_tools = manager.init_config(mcp_config)
416-
417-
# Convert to OpenAI format
418-
return _convert_fns_to_tools(mcp_tools)
415+
try:
416+
mcp_tools = manager.init_config(mcp_config)
417+
418+
# Convert to OpenAI format
419+
return _convert_fns_to_tools(mcp_tools)
420+
421+
except Exception as e:
422+
# Provide helpful error message based on the error type
423+
error_msg = str(e)
424+
425+
if "No such file or directory" in error_msg:
426+
# Common issue: MCP server command not found
427+
if "uvx" in error_msg:
428+
raise ValueError(
429+
f"Failed to initialize MCP tools: {e}. "
430+
"The 'uvx' command is not installed. Install it with: pip install uvx"
431+
) from e
432+
elif "npx" in error_msg:
433+
raise ValueError(
434+
f"Failed to initialize MCP tools: {e}. "
435+
"The 'npx' command is not installed. Install Node.js and npm."
436+
) from e
437+
else:
438+
raise ValueError(
439+
f"Failed to initialize MCP tools: {e}. "
440+
"Check that the MCP server command is installed and accessible."
441+
) from e
442+
elif "fileno" in error_msg:
443+
# File descriptor related error - usually subprocess communication issue
444+
raise ValueError(
445+
f"Failed to initialize MCP tools: {e}. "
446+
"This may be due to a subprocess communication issue. "
447+
"Check that the MCP server command is properly configured."
448+
) from e
449+
else:
450+
# Generic MCP initialization error
451+
raise ValueError(f"Failed to initialize MCP tools: {e}") from e
419452

420453

421454
def ensure_openai_format(tools: Optional[Union[List[Union[Dict[str, Any], str]], List[Fn], str]]) -> Optional[List[Dict[str, Any]]]:
@@ -465,8 +498,17 @@ def ensure_openai_format(tools: Optional[Union[List[Union[Dict[str, Any], str]],
465498

466499
# Handle MCP servers configuration
467500
elif isinstance(item, dict) and "mcpServers" in item:
468-
mcp_tools = _handle_mcp_servers_config(item)
469-
all_tools.extend(mcp_tools)
501+
try:
502+
mcp_tools = _handle_mcp_servers_config(item)
503+
all_tools.extend(mcp_tools)
504+
except Exception as e:
505+
# Instead of failing completely, just warn and continue with other tools
506+
import warnings
507+
warnings.warn(
508+
f"Failed to initialize MCP tools: {e}. "
509+
f"Continuing with other available tools."
510+
)
511+
# Continue processing other tools instead of failing
470512

471513
# Handle regular OpenAI tool format
472514
elif isinstance(item, dict) and "type" in item:

HelpingAI/tools/mcp_manager.py

Lines changed: 113 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -173,87 +173,124 @@ async def _init_config_async(self, config: Dict[str, Any]) -> List[Fn]:
173173
"""Async implementation of MCP configuration initialization."""
174174
tools: List[Fn] = []
175175
mcp_servers = config['mcpServers']
176+
successful_connections = 0
177+
failed_connections = []
176178

177179
for server_name, server_config in mcp_servers.items():
178-
client = MCPClient()
179-
180-
# Connect to the MCP server
181-
await client.connect_server(server_name, server_config)
182-
183-
# Generate unique client ID
184-
client_id = f"{server_name}_{uuid.uuid4()}"
185-
client.client_id = client_id
186-
self.clients[client_id] = client
187-
188-
# Convert MCP tools to Fn objects
189-
for mcp_tool in client.tools:
190-
# Create tool parameters schema
191-
parameters = mcp_tool.inputSchema
192-
if 'required' not in parameters:
193-
parameters['required'] = []
180+
try:
181+
client = MCPClient()
194182

195-
# Ensure schema has required fields
196-
required_fields = {'type', 'properties', 'required'}
197-
missing_fields = required_fields - parameters.keys()
198-
if missing_fields:
199-
raise ValueError(f'Missing required schema fields: {missing_fields}')
200-
201-
# Clean up parameters to only include standard fields
202-
cleaned_parameters = {
203-
'type': parameters['type'],
204-
'properties': parameters['properties'],
205-
'required': parameters['required']
206-
}
183+
# Connect to the MCP server
184+
await client.connect_server(server_name, server_config)
207185

208-
# Create tool name and Fn object
209-
tool_name = f"{server_name}-{mcp_tool.name}"
210-
fn_obj = self._create_mcp_tool_fn(
211-
name=tool_name,
212-
client_id=client_id,
213-
mcp_tool_name=mcp_tool.name,
214-
description=mcp_tool.description,
215-
parameters=cleaned_parameters
216-
)
217-
tools.append(fn_obj)
218-
219-
# Add resource tools if available
220-
if client.resources:
221-
# List resources tool
222-
list_resources_name = f"{server_name}-list_resources"
223-
list_resources_fn = self._create_mcp_tool_fn(
224-
name=list_resources_name,
225-
client_id=client_id,
226-
mcp_tool_name='list_resources',
227-
description=(
228-
'List available resources from the MCP server. '
229-
'Resources represent data sources that can be used as context.'
230-
),
231-
parameters={'type': 'object', 'properties': {}, 'required': []}
232-
)
233-
tools.append(list_resources_fn)
234-
235-
# Read resource tool
236-
read_resource_name = f"{server_name}-read_resource"
237-
read_resource_fn = self._create_mcp_tool_fn(
238-
name=read_resource_name,
239-
client_id=client_id,
240-
mcp_tool_name='read_resource',
241-
description=(
242-
'Read a specific resource by URI. '
243-
'Use list_resources first to discover available URIs.'
244-
),
245-
parameters={
246-
'type': 'object',
247-
'properties': {
248-
'uri': {
249-
'type': 'string',
250-
'description': 'The URI of the resource to read'
251-
}
252-
},
253-
'required': ['uri']
186+
# Generate unique client ID
187+
client_id = f"{server_name}_{uuid.uuid4()}"
188+
client.client_id = client_id
189+
self.clients[client_id] = client
190+
successful_connections += 1
191+
192+
# Convert MCP tools to Fn objects
193+
for mcp_tool in client.tools:
194+
# Create tool parameters schema
195+
parameters = mcp_tool.inputSchema
196+
if 'required' not in parameters:
197+
parameters['required'] = []
198+
199+
# Ensure schema has required fields
200+
required_fields = {'type', 'properties', 'required'}
201+
missing_fields = required_fields - parameters.keys()
202+
if missing_fields:
203+
raise ValueError(f'Missing required schema fields: {missing_fields}')
204+
205+
# Clean up parameters to only include standard fields
206+
cleaned_parameters = {
207+
'type': parameters['type'],
208+
'properties': parameters['properties'],
209+
'required': parameters['required']
254210
}
255-
)
256-
tools.append(read_resource_fn)
211+
212+
# Create tool name and Fn object
213+
tool_name = f"{server_name}-{mcp_tool.name}"
214+
fn_obj = self._create_mcp_tool_fn(
215+
name=tool_name,
216+
client_id=client_id,
217+
mcp_tool_name=mcp_tool.name,
218+
description=mcp_tool.description,
219+
parameters=cleaned_parameters
220+
)
221+
tools.append(fn_obj)
222+
223+
# Add resource tools if available
224+
if client.resources:
225+
# List resources tool
226+
list_resources_name = f"{server_name}-list_resources"
227+
list_resources_fn = self._create_mcp_tool_fn(
228+
name=list_resources_name,
229+
client_id=client_id,
230+
mcp_tool_name='list_resources',
231+
description=(
232+
'List available resources from the MCP server. '
233+
'Resources represent data sources that can be used as context.'
234+
),
235+
parameters={'type': 'object', 'properties': {}, 'required': []}
236+
)
237+
tools.append(list_resources_fn)
238+
239+
# Read resource tool
240+
read_resource_name = f"{server_name}-read_resource"
241+
read_resource_fn = self._create_mcp_tool_fn(
242+
name=read_resource_name,
243+
client_id=client_id,
244+
mcp_tool_name='read_resource',
245+
description=(
246+
'Read a specific resource by URI. '
247+
'Use list_resources first to discover available URIs.'
248+
),
249+
parameters={
250+
'type': 'object',
251+
'properties': {
252+
'uri': {
253+
'type': 'string',
254+
'description': 'The URI of the resource to read'
255+
}
256+
},
257+
'required': ['uri']
258+
}
259+
)
260+
tools.append(read_resource_fn)
261+
262+
except Exception as e:
263+
# Log the failed connection but continue with other servers
264+
failed_connections.append((server_name, str(e)))
265+
continue
266+
267+
# If no servers connected successfully, raise an error with helpful details
268+
if successful_connections == 0:
269+
error_details = []
270+
for server_name, error in failed_connections:
271+
error_details.append(f" - {server_name}: {error}")
272+
273+
error_msg = f"Failed to connect to any MCP servers:\n" + "\n".join(error_details)
274+
275+
# Provide helpful suggestions based on common errors
276+
if any("No such file or directory" in error for _, error in failed_connections):
277+
error_msg += "\n\nCommon solutions:"
278+
if any("uvx" in error for _, error in failed_connections):
279+
error_msg += "\n - Install uvx: pip install uvx"
280+
if any("npx" in error for _, error in failed_connections):
281+
error_msg += "\n - Install Node.js and npm"
282+
error_msg += "\n - Check that MCP server commands are in your PATH"
283+
284+
raise HAIError(error_msg)
285+
286+
# If some servers failed but others succeeded, just warn
287+
if failed_connections:
288+
import warnings
289+
failed_names = [name for name, _ in failed_connections]
290+
warnings.warn(
291+
f"Some MCP servers failed to connect: {', '.join(failed_names)}. "
292+
f"Continuing with {successful_connections} successful connection(s)."
293+
)
257294

258295
return tools
259296

test_conversion.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test tool conversion without making actual API calls.
4+
"""
5+
6+
from HelpingAI import HAI
7+
8+
def test_tool_conversion():
9+
"""Test the tool conversion process without API calls."""
10+
11+
client = HAI(api_key="test-key")
12+
13+
# The problematic configuration from the issue
14+
tools = [
15+
{'mcpServers': {
16+
'time': {'command': 'uvx', 'args': ['mcp-server-time']},
17+
'fetch': {'command': 'uvx', 'args': ['mcp-server-fetch']},
18+
'ddg-search': {
19+
'command': 'npx',
20+
'args': ['-y', '@oevortex/ddg_search@latest']
21+
}
22+
}},
23+
'code_interpreter',
24+
]
25+
26+
print("Testing tool conversion process...")
27+
28+
# Test the conversion directly
29+
try:
30+
converted_tools = client._convert_tools_parameter(tools)
31+
32+
if converted_tools:
33+
print(f"✅ Tool conversion successful! Converted {len(converted_tools)} tools:")
34+
for i, tool in enumerate(converted_tools):
35+
tool_name = tool.get('function', {}).get('name', 'Unknown')
36+
print(f" {i+1}. {tool_name}")
37+
else:
38+
print("❌ Tool conversion returned None")
39+
40+
except Exception as e:
41+
print(f"❌ Tool conversion failed: {e}")
42+
43+
if __name__ == "__main__":
44+
test_tool_conversion()

0 commit comments

Comments
 (0)