bpHigh commited on
Commit
fc0d268
·
1 Parent(s): d16322a

Revamp stuff

Browse files
Files changed (2) hide show
  1. app.py +11 -10
  2. utils/huggingface_mcp_llamaindex.py +444 -230
app.py CHANGED
@@ -7,7 +7,7 @@ from utils.google_genai_llm import get_response, generate_with_gemini
7
  from utils.utils import parse_json_codefences
8
  from prompts.requirements_gathering import requirements_gathering_system_prompt
9
  from prompts.planning import hf_query_gen_prompt, hf_context_gen_prompt
10
- from utils.huggingface_mcp_llamaindex import get_hf_tools, call_hf_tool, diagnose_connection
11
  from prompts.devstral_coding_prompt import devstral_code_gen_sys_prompt, devstral_code_gen_user_prompt
12
  from dotenv import load_dotenv
13
  import os
@@ -50,6 +50,9 @@ BEARER_TOKEN = os.getenv("BEARER_TOKEN")
50
  CODING_MODEL = os.getenv("CODING_MODEL")
51
 
52
  MCP_TOKEN = os.getenv("MCP_TOKEN")
 
 
 
53
  def get_file_hash(file_path):
54
  """Generate a hash of the file for caching purposes"""
55
  try:
@@ -245,19 +248,17 @@ async def generate_plan(history, file_cache):
245
  if ai_msg:
246
  conversation_history += f"Assistant: {ai_msg}\n"
247
 
248
- print("Running connection diagnostics...")
249
- diagnostics = await diagnose_connection(MCP_TOKEN)
250
  print(f"Diagnostics: {json.dumps(diagnostics, indent=2)}")
251
 
252
- if not diagnostics["connection_test"]:
253
  print("Basic connection failed - check token and network")
254
- return
255
 
256
- if not diagnostics["tools_test"]:
257
- print("Tools retrieval failed - check server status")
258
- return
259
  # try:
260
- hf_query_gen_tool_details = await get_hf_tools(hf_token=MCP_TOKEN)
261
  # except Exception as e:
262
  # hf_query_gen_tool_details = """meta=None nextCursor=None tools=[Tool(name='hf_whoami', description="Hugging Face tools are being used by authenticated user 'bpHigh'", inputSchema={'type': 'object', 'properties': {}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Hugging Face User Info', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=None)), Tool(name='space_search', description='Find Hugging Face Spaces using semantic search. Include links to the Space when presenting the results.', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'minLength': 1, 'maxLength': 50, 'description': 'Semantic Search Query'}, 'limit': {'type': 'number', 'default': 10, 'description': 'Number of results to return'}, 'mcp': {'type': 'boolean', 'default': False, 'description': 'Only return MCP Server enabled Spaces'}}, 'required': ['query'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Hugging Face Space Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='model_search', description='Find Machine Learning models hosted on Hugging Face. Returns comprehensive information about matching models including downloads, likes, tags, and direct links. Include links to the models in your response', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search term. Leave blank and specify "sort" and "limit" to get e.g. "Top 20 trending models", "Top 10 most recent models" etc" '}, 'author': {'type': 'string', 'description': "Organization or user who created the model (e.g., 'google', 'meta-llama', 'microsoft')"}, 'task': {'type': 'string', 'description': "Model task type (e.g., 'text-generation', 'image-classification', 'translation')"}, 'library': {'type': 'string', 'description': "Framework the model uses (e.g., 'transformers', 'diffusers', 'timm')"}, 'sort': {'type': 'string', 'enum': ['trendingScore', 'downloads', 'likes', 'createdAt', 'lastModified'], 'description': 'Sort order: trendingScore, downloads , likes, createdAt, lastModified'}, 'limit': {'type': 'number', 'minimum': 1, 'maximum': 100, 'default': 20, 'description': 'Maximum number of results to return'}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Model Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='model_details', description='Get detailed information about a specific model from the Hugging Face Hub.', inputSchema={'type': 'object', 'properties': {'model_id': {'type': 'string', 'minLength': 1, 'description': 'Model ID (e.g., microsoft/DialoGPT-large)'}}, 'required': ['model_id'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Model Details', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=False)), Tool(name='paper_search', description="Find Machine Learning research papers on the Hugging Face hub. Include 'Link to paper' When presenting the results. Consider whether tabulating results matches user intent.", inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'minLength': 3, 'maxLength': 200, 'description': 'Semantic Search query'}, 'results_limit': {'type': 'number', 'default': 12, 'description': 'Number of results to return'}, 'concise_only': {'type': 'boolean', 'default': False, 'description': 'Return a 2 sentence summary of the abstract. Use for broad search terms which may return a lot of results. Check with User if unsure.'}}, 'required': ['query'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Paper Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='dataset_search', description='Find Datasets hosted on the Hugging Face hub. Returns comprehensive information about matching datasets including downloads, likes, tags, and direct links. Include links to the datasets in your response', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search term. Leave blank and specify "sort" and "limit" to get e.g. "Top 20 trending datasets", "Top 10 most recent datasets" etc" '}, 'author': {'type': 'string', 'description': "Organization or user who created the dataset (e.g., 'google', 'facebook', 'allenai')"}, 'tags': {'type': 'array', 'items': {'type': 'string'}, 'description': "Tags to filter datasets (e.g., ['language:en', 'size_categories:1M<n<10M', 'task_categories:text-classification'])"}, 'sort': {'type': 'string', 'enum': ['trendingScore', 'downloads', 'likes', 'createdAt', 'lastModified'], 'description': 'Sort order: trendingScore, downloads, likes, createdAt, lastModified'}, 'limit': {'type': 'number', 'minimum': 1, 'maximum': 100, 'default': 20, 'description': 'Maximum number of results to return'}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Dataset Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='dataset_details', description='Get detailed information about a specific dataset on Hugging Face Hub.', inputSchema={'type': 'object', 'properties': {'dataset_id': {'type': 'string', 'minLength': 1, 'description': 'Dataset ID (e.g., squad, glue, imdb)'}}, 'required': ['dataset_id'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Dataset Details', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=False)), Tool(name='gr1_evalstate_flux1_schnell', description='Generate an image using the Flux 1 Schnell Image Generator. (from evalstate/flux1_schnell)', inputSchema={'type': 'object', 'properties': {'prompt': {'type': 'string'}, 'seed': {'type': 'number', 'description': 'numeric value between 0 and 2147483647'}, 'randomize_seed': {'type': 'boolean', 'default': True}, 'width': {'type': 'number', 'description': 'numeric value between 256 and 2048', 'default': 1024}, 'height': {'type': 'number', 'description': 'numeric value between 256 and 2048', 'default': 1024}, 'num_inference_steps': {'type': 'number', 'description': 'numeric value between 1 and 50', 'default': 4}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='evalstate/flux1_schnell - flux1_schnell_infer 🏎️💨', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=True)), Tool(name='gr2_abidlabs_easyghibli', description='Convert an image into a Studio Ghibli style image (from abidlabs/EasyGhibli)', inputSchema={'type': 'object', 'properties': {'spatial_img': {'type': 'string', 'description': 'File input: provide URL or file path'}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='abidlabs/EasyGhibli - abidlabs_EasyGhiblisingle_condition_generate_image 🦀', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=True)), Tool(name='gr3_linoyts_framepack_f1', description='FramePack_F1_end_process tool from linoyts/FramePack-F1', inputSchema={'type': 'object', 'properties': {}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='linoyts/FramePack-F1 - FramePack_F1_end_process 📹⚡️', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=True))]"""
263
  # print(str(e))
@@ -273,7 +274,7 @@ async def generate_plan(history, file_cache):
273
 
274
  # Call tool to get tool calls
275
  try:
276
- tool_calls = await asyncio.gather(*[call_hf_tool(MCP_TOKEN, step['tool'], step['args']) for step in parsed_plan])
277
  except Exception as e:
278
  tool_calls = []
279
  print(tool_calls)
 
7
  from utils.utils import parse_json_codefences
8
  from prompts.requirements_gathering import requirements_gathering_system_prompt
9
  from prompts.planning import hf_query_gen_prompt, hf_context_gen_prompt
10
+ from utils.huggingface_mcp_llamaindex import get_hf_tools, call_hf_tool, diagnose_connection_advanced, get_hf_tools_robust,call_hf_tool_robust
11
  from prompts.devstral_coding_prompt import devstral_code_gen_sys_prompt, devstral_code_gen_user_prompt
12
  from dotenv import load_dotenv
13
  import os
 
50
  CODING_MODEL = os.getenv("CODING_MODEL")
51
 
52
  MCP_TOKEN = os.getenv("MCP_TOKEN")
53
+ if not MCP_TOKEN:
54
+ print("Please set MCP_TOKEN")
55
+
56
  def get_file_hash(file_path):
57
  """Generate a hash of the file for caching purposes"""
58
  try:
 
248
  if ai_msg:
249
  conversation_history += f"Assistant: {ai_msg}\n"
250
 
251
+ print("Running advanced connection diagnostics...")
252
+ diagnostics = await diagnose_connection_advanced(MCP_TOKEN)
253
  print(f"Diagnostics: {json.dumps(diagnostics, indent=2)}")
254
 
255
+ if not diagnostics["tests"]["basic_connection"]:
256
  print("Basic connection failed - check token and network")
 
257
 
258
+
259
+
 
260
  # try:
261
+ hf_query_gen_tool_details = await get_hf_tools_robust(hf_token=MCP_TOKEN)
262
  # except Exception as e:
263
  # hf_query_gen_tool_details = """meta=None nextCursor=None tools=[Tool(name='hf_whoami', description="Hugging Face tools are being used by authenticated user 'bpHigh'", inputSchema={'type': 'object', 'properties': {}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Hugging Face User Info', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=None)), Tool(name='space_search', description='Find Hugging Face Spaces using semantic search. Include links to the Space when presenting the results.', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'minLength': 1, 'maxLength': 50, 'description': 'Semantic Search Query'}, 'limit': {'type': 'number', 'default': 10, 'description': 'Number of results to return'}, 'mcp': {'type': 'boolean', 'default': False, 'description': 'Only return MCP Server enabled Spaces'}}, 'required': ['query'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Hugging Face Space Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='model_search', description='Find Machine Learning models hosted on Hugging Face. Returns comprehensive information about matching models including downloads, likes, tags, and direct links. Include links to the models in your response', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search term. Leave blank and specify "sort" and "limit" to get e.g. "Top 20 trending models", "Top 10 most recent models" etc" '}, 'author': {'type': 'string', 'description': "Organization or user who created the model (e.g., 'google', 'meta-llama', 'microsoft')"}, 'task': {'type': 'string', 'description': "Model task type (e.g., 'text-generation', 'image-classification', 'translation')"}, 'library': {'type': 'string', 'description': "Framework the model uses (e.g., 'transformers', 'diffusers', 'timm')"}, 'sort': {'type': 'string', 'enum': ['trendingScore', 'downloads', 'likes', 'createdAt', 'lastModified'], 'description': 'Sort order: trendingScore, downloads , likes, createdAt, lastModified'}, 'limit': {'type': 'number', 'minimum': 1, 'maximum': 100, 'default': 20, 'description': 'Maximum number of results to return'}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Model Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='model_details', description='Get detailed information about a specific model from the Hugging Face Hub.', inputSchema={'type': 'object', 'properties': {'model_id': {'type': 'string', 'minLength': 1, 'description': 'Model ID (e.g., microsoft/DialoGPT-large)'}}, 'required': ['model_id'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Model Details', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=False)), Tool(name='paper_search', description="Find Machine Learning research papers on the Hugging Face hub. Include 'Link to paper' When presenting the results. Consider whether tabulating results matches user intent.", inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'minLength': 3, 'maxLength': 200, 'description': 'Semantic Search query'}, 'results_limit': {'type': 'number', 'default': 12, 'description': 'Number of results to return'}, 'concise_only': {'type': 'boolean', 'default': False, 'description': 'Return a 2 sentence summary of the abstract. Use for broad search terms which may return a lot of results. Check with User if unsure.'}}, 'required': ['query'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Paper Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='dataset_search', description='Find Datasets hosted on the Hugging Face hub. Returns comprehensive information about matching datasets including downloads, likes, tags, and direct links. Include links to the datasets in your response', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search term. Leave blank and specify "sort" and "limit" to get e.g. "Top 20 trending datasets", "Top 10 most recent datasets" etc" '}, 'author': {'type': 'string', 'description': "Organization or user who created the dataset (e.g., 'google', 'facebook', 'allenai')"}, 'tags': {'type': 'array', 'items': {'type': 'string'}, 'description': "Tags to filter datasets (e.g., ['language:en', 'size_categories:1M<n<10M', 'task_categories:text-classification'])"}, 'sort': {'type': 'string', 'enum': ['trendingScore', 'downloads', 'likes', 'createdAt', 'lastModified'], 'description': 'Sort order: trendingScore, downloads, likes, createdAt, lastModified'}, 'limit': {'type': 'number', 'minimum': 1, 'maximum': 100, 'default': 20, 'description': 'Maximum number of results to return'}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Dataset Search', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=True)), Tool(name='dataset_details', description='Get detailed information about a specific dataset on Hugging Face Hub.', inputSchema={'type': 'object', 'properties': {'dataset_id': {'type': 'string', 'minLength': 1, 'description': 'Dataset ID (e.g., squad, glue, imdb)'}}, 'required': ['dataset_id'], 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='Dataset Details', readOnlyHint=True, destructiveHint=False, idempotentHint=None, openWorldHint=False)), Tool(name='gr1_evalstate_flux1_schnell', description='Generate an image using the Flux 1 Schnell Image Generator. (from evalstate/flux1_schnell)', inputSchema={'type': 'object', 'properties': {'prompt': {'type': 'string'}, 'seed': {'type': 'number', 'description': 'numeric value between 0 and 2147483647'}, 'randomize_seed': {'type': 'boolean', 'default': True}, 'width': {'type': 'number', 'description': 'numeric value between 256 and 2048', 'default': 1024}, 'height': {'type': 'number', 'description': 'numeric value between 256 and 2048', 'default': 1024}, 'num_inference_steps': {'type': 'number', 'description': 'numeric value between 1 and 50', 'default': 4}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='evalstate/flux1_schnell - flux1_schnell_infer 🏎️💨', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=True)), Tool(name='gr2_abidlabs_easyghibli', description='Convert an image into a Studio Ghibli style image (from abidlabs/EasyGhibli)', inputSchema={'type': 'object', 'properties': {'spatial_img': {'type': 'string', 'description': 'File input: provide URL or file path'}}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='abidlabs/EasyGhibli - abidlabs_EasyGhiblisingle_condition_generate_image 🦀', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=True)), Tool(name='gr3_linoyts_framepack_f1', description='FramePack_F1_end_process tool from linoyts/FramePack-F1', inputSchema={'type': 'object', 'properties': {}, 'additionalProperties': False, '$schema': 'http://json-schema.org/draft-07/schema#'}, annotations=ToolAnnotations(title='linoyts/FramePack-F1 - FramePack_F1_end_process 📹⚡️', readOnlyHint=None, destructiveHint=None, idempotentHint=None, openWorldHint=True))]"""
264
  # print(str(e))
 
274
 
275
  # Call tool to get tool calls
276
  try:
277
+ tool_calls = await asyncio.gather(*[call_hf_tool_robust(MCP_TOKEN, step['tool'], step['args']) for step in parsed_plan])
278
  except Exception as e:
279
  tool_calls = []
280
  print(tool_calls)
utils/huggingface_mcp_llamaindex.py CHANGED
@@ -1,9 +1,17 @@
 
 
 
 
 
 
 
1
  import asyncio
2
  import json
3
  import logging
4
  import os
5
- from typing import Any, Dict, List, Optional
6
  from datetime import timedelta
 
7
 
8
  from mcp.shared.message import SessionMessage
9
  from mcp.types import (
@@ -18,230 +26,177 @@ from mcp.client.streamable_http import streamablehttp_client
18
  logger = logging.getLogger(__name__)
19
 
20
 
21
- class HuggingFaceMCPClient:
22
- """Client for interacting with Hugging Face MCP endpoint."""
23
 
24
- def __init__(self, hf_token: str, timeout: int = 60):
25
  """
26
- Initialize the Hugging Face MCP client.
27
 
28
  Args:
29
  hf_token: Hugging Face API token
30
- timeout: Timeout in seconds for HTTP requests (increased for Spaces)
31
  """
32
  self.hf_token = hf_token
33
  self.url = "https://huggingface.co/mcp"
34
  self.headers = {
35
  "Authorization": f"Bearer {hf_token}",
36
- "User-Agent": "hf-mcp-client/1.0.0" # Add user agent
 
 
37
  }
38
  self.timeout = timedelta(seconds=timeout)
39
- self.sse_read_timeout = timedelta(seconds=timeout * 2) # Longer SSE timeout
40
  self.request_id_counter = 0
41
 
42
  def _get_next_request_id(self) -> int:
43
  """Get the next request ID."""
44
  self.request_id_counter += 1
45
  return self.request_id_counter
46
-
47
- async def _send_request_and_get_response(
48
  self,
49
  method: str,
50
- params: Optional[Dict[str, Any]] = None,
51
- max_retries: int = 3
52
  ) -> Any:
53
  """
54
- Send a JSON-RPC request and wait for the response with retry logic.
55
-
56
- Args:
57
- method: The JSON-RPC method name
58
- params: Optional parameters for the method
59
- max_retries: Maximum number of retry attempts
60
-
61
- Returns:
62
- The response result or raises an exception
63
  """
64
- last_exception = None
65
-
66
- for attempt in range(max_retries):
67
- try:
68
- return await self._attempt_request(method, params)
69
- except Exception as e:
70
- last_exception = e
71
- logger.warning(f"Attempt {attempt + 1} failed: {e}")
72
- if attempt < max_retries - 1:
73
- await asyncio.sleep(2 ** attempt) # Exponential backoff
74
-
75
- raise last_exception
76
-
77
- async def _attempt_request(self, method: str, params: Optional[Dict[str, Any]]) -> Any:
78
- """Single attempt to send request."""
79
  request_id = self._get_next_request_id()
80
 
81
- # Create JSON-RPC request
82
- jsonrpc_request = JSONRPCRequest(
83
  jsonrpc="2.0",
84
  id=request_id,
85
  method=method,
86
  params=params
87
  )
88
 
89
- message = JSONRPCMessage(jsonrpc_request)
90
- session_message = SessionMessage(message)
91
-
92
  async with streamablehttp_client(
93
  url=self.url,
94
  headers=self.headers,
95
  timeout=self.timeout,
96
  sse_read_timeout=self.sse_read_timeout,
97
- terminate_on_close=True
98
  ) as (read_stream, write_stream, get_session_id):
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  try:
101
- # Send initialization request first with proper client info
102
- init_request = JSONRPCRequest(
103
- jsonrpc="2.0",
104
- id=self._get_next_request_id(),
105
- method="initialize",
106
- params={
107
- "protocolVersion": "2024-11-05",
108
- "capabilities": {
109
- "tools": {},
110
- "resources": {},
111
- "prompts": {}
112
- },
113
- "clientInfo": {
114
- "name": "hf-mcp-client",
115
- "version": "1.0.0"
116
- }
117
- }
118
- )
119
-
120
- init_message = JSONRPCMessage(init_request)
121
- init_session_message = SessionMessage(init_message)
122
-
123
- logger.info("Sending initialization request...")
124
- await write_stream.send(init_session_message)
125
-
126
- # Wait for initialization response with better error handling
127
- init_response_received = False
128
- timeout_counter = 0
129
- max_iterations = 150 # Increased for Spaces environment
130
-
131
- while not init_response_received and timeout_counter < max_iterations:
132
- try:
133
- # Use asyncio.wait_for to add timeout per receive operation
134
- response = await asyncio.wait_for(
135
- read_stream.receive(),
136
- timeout=30.0 # 30 second timeout per receive
137
- )
138
- timeout_counter += 1
139
-
140
- if isinstance(response, Exception):
141
- logger.error(f"Received exception during init: {response}")
142
- raise response
143
-
144
- if isinstance(response, SessionMessage):
145
- msg = response.message.root
146
- if isinstance(msg, JSONRPCResponse) and msg.id == init_request.id:
147
- logger.info("MCP client initialized successfully")
148
- init_response_received = True
149
- # Log the session ID if available
150
- session_id = get_session_id()
151
- if session_id:
152
- logger.info(f"Session ID: {session_id}")
153
- elif isinstance(msg, JSONRPCError) and msg.id == init_request.id:
154
- error_msg = f"Initialization failed: {msg.error}"
155
- logger.error(error_msg)
156
- raise Exception(error_msg)
157
- else:
158
- logger.debug(f"Received other message during init: {type(msg)}")
159
-
160
- except asyncio.TimeoutError:
161
- logger.warning(f"Timeout waiting for response (attempt {timeout_counter})")
162
- if timeout_counter > 10: # After 10 timeouts, give up
163
- raise Exception("Initialization timeout: no response from server")
164
- except Exception as e:
165
- if "ClosedResourceError" in str(type(e)) or "StreamClosed" in str(e):
166
- logger.error("Stream closed during initialization")
167
- raise Exception("Connection closed during initialization")
168
- logger.error(f"Error during initialization: {e}")
169
- raise
170
-
171
- if not init_response_received:
172
- raise Exception("Initialization timeout: maximum iterations reached")
173
-
174
- # Send initialized notification
175
- initialized_notification = JSONRPCNotification(
176
- jsonrpc="2.0",
177
- method="notifications/initialized"
178
  )
179
 
180
- init_notif_message = JSONRPCMessage(initialized_notification)
181
- init_notif_session_message = SessionMessage(init_notif_message)
182
-
183
- logger.info("Sending initialized notification...")
184
- await write_stream.send(init_notif_session_message)
185
-
186
- # Longer delay for Spaces environment
187
- await asyncio.sleep(1.0)
188
-
189
- # Now send our actual request
190
- logger.info(f"Sending actual request: {method}")
191
- await write_stream.send(session_message)
192
-
193
- # Wait for the response to our request
194
- response_received = False
195
- timeout_counter = 0
196
-
197
- while not response_received and timeout_counter < max_iterations:
198
- try:
199
- response = await asyncio.wait_for(
200
- read_stream.receive(),
201
- timeout=30.0
202
- )
203
- timeout_counter += 1
204
-
205
- if isinstance(response, Exception):
206
- logger.error(f"Received exception during request: {response}")
207
- raise response
208
-
209
- if isinstance(response, SessionMessage):
210
- msg = response.message.root
211
- if isinstance(msg, JSONRPCResponse) and msg.id == request_id:
212
- logger.info(f"Request '{method}' completed successfully")
213
- return msg.result
214
- elif isinstance(msg, JSONRPCError) and msg.id == request_id:
215
- error_msg = f"Request failed: {msg.error}"
216
- logger.error(error_msg)
217
- raise Exception(error_msg)
218
- else:
219
- logger.debug(f"Received other message during request: {type(msg)}")
220
-
221
- except asyncio.TimeoutError:
222
- logger.warning(f"Timeout waiting for request response (attempt {timeout_counter})")
223
- if timeout_counter > 10:
224
- raise Exception("Request timeout: no response from server")
225
- except Exception as e:
226
- if "ClosedResourceError" in str(type(e)) or "StreamClosed" in str(e):
227
- logger.error("Stream closed during request processing")
228
- raise Exception("Connection closed during request processing")
229
- logger.error(f"Error during request processing: {e}")
230
- raise
231
 
232
- if not response_received:
233
- raise Exception("Request timeout: maximum iterations reached")
 
 
 
 
 
 
 
 
 
234
 
 
 
 
 
 
 
 
 
 
235
  except Exception as e:
236
- logger.error(f"Error during MCP communication: {e}")
 
 
237
  raise
238
- finally:
239
- # Ensure streams are properly closed
240
- try:
241
- await write_stream.aclose()
242
- except Exception as close_error:
243
- logger.debug(f"Error closing write stream: {close_error}")
244
-
245
  async def get_all_tools(self) -> List[Dict[str, Any]]:
246
  """
247
  Get all available tools from the Hugging Face MCP endpoint.
@@ -251,20 +206,20 @@ class HuggingFaceMCPClient:
251
  """
252
  try:
253
  logger.info("Fetching all available tools from Hugging Face MCP")
254
- result = await self._send_request_and_get_response("tools/list")
255
 
256
  if isinstance(result, dict) and "tools" in result:
257
  tools = result["tools"]
258
- logger.info(f"Found {len(tools)} available tools")
259
  return tools
260
  else:
261
- logger.warning(f"Unexpected response format: {result}")
262
  return []
263
 
264
  except Exception as e:
265
  logger.error(f"Failed to get tools: {e}")
266
  raise
267
-
268
  async def call_tool(self, tool_name: str, args: Dict[str, Any]) -> Any:
269
  """
270
  Call a specific tool with the given arguments.
@@ -284,7 +239,7 @@ class HuggingFaceMCPClient:
284
  "arguments": args
285
  }
286
 
287
- result = await self._send_request_and_get_response("tools/call", params)
288
  logger.info(f"Tool '{tool_name}' executed successfully")
289
  return result
290
 
@@ -293,92 +248,351 @@ class HuggingFaceMCPClient:
293
  raise
294
 
295
 
296
- # Convenience functions for easier usage
297
- async def get_hf_tools(hf_token: str) -> List[Dict[str, Any]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  """
299
- Get all available tools from Hugging Face MCP.
300
 
301
  Args:
302
  hf_token: Hugging Face API token
 
303
 
304
  Returns:
305
  List of tool definitions
306
  """
307
- client = HuggingFaceMCPClient(hf_token, timeout=90) # Longer timeout for Spaces
308
- return await client.get_all_tools()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
 
311
- async def call_hf_tool(hf_token: str, tool_name: str, args: Dict[str, Any]) -> Any:
 
 
 
 
 
312
  """
313
- Call a specific Hugging Face MCP tool.
314
 
315
  Args:
316
  hf_token: Hugging Face API token
317
  tool_name: Name of the tool to call
318
  args: Arguments to pass to the tool
 
319
 
320
  Returns:
321
  The tool's response
322
  """
323
- client = HuggingFaceMCPClient(hf_token, timeout=90)
324
- return await client.call_tool(tool_name, args)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
 
327
- # Diagnostic function for Spaces environment
328
- async def diagnose_connection(hf_token: str) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
329
  """
330
- Diagnose connection issues with detailed logging.
331
 
332
  Args:
333
  hf_token: Hugging Face API token
334
 
335
  Returns:
336
- Diagnostic information
337
  """
338
  diagnostics = {
339
  "environment": "huggingface_spaces" if os.getenv("SPACE_ID") else "local",
340
  "space_id": os.getenv("SPACE_ID"),
 
341
  "token_length": len(hf_token) if hf_token else 0,
342
  "has_token": bool(hf_token),
343
- "connection_test": False,
344
- "initialization_test": False,
345
- "tools_test": False,
346
- "error": None
 
 
 
 
 
 
347
  }
348
 
 
349
  try:
350
- # Test basic connection
351
- from mcp.client.streamable_http import streamablehttp_client
352
-
353
- headers = {
354
- "Authorization": f"Bearer {hf_token}",
355
- "User-Agent": "hf-mcp-diagnostic/1.0.0"
356
- }
357
-
358
  async with streamablehttp_client(
359
  url="https://huggingface.co/mcp",
360
- headers=headers,
361
- timeout=timedelta(seconds=30),
362
- terminate_on_close=True
363
  ) as (read_stream, write_stream, get_session_id):
364
- diagnostics["connection_test"] = True
365
  logger.info("Basic connection test passed")
366
-
367
- # Test initialization
368
- client = HuggingFaceMCPClient(hf_token, timeout=60)
369
- try:
370
- tools = await client.get_all_tools()
371
- diagnostics["initialization_test"] = True
372
- diagnostics["tools_test"] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  diagnostics["tool_count"] = len(tools)
374
- logger.info(f"Full diagnostic passed - found {len(tools)} tools")
375
- except Exception as init_error:
376
- diagnostics["error"] = str(init_error)
377
- logger.error(f"Initialization failed: {init_error}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
- except Exception as conn_error:
380
- diagnostics["error"] = str(conn_error)
381
- logger.error(f"Connection test failed: {conn_error}")
 
 
 
 
 
 
 
 
 
 
382
 
383
- return diagnostics
384
-
 
1
+ """
2
+ Robust Hugging Face MCP Client - Optimized for HF Spaces
3
+
4
+ This module provides a robust client for interacting with Hugging Face's MCP endpoint
5
+ with better error handling, TaskGroup avoidance, and compatibility for Hugging Face Spaces.
6
+ """
7
+
8
  import asyncio
9
  import json
10
  import logging
11
  import os
12
+ from typing import Any, Dict, List, Optional, Union
13
  from datetime import timedelta
14
+ from contextlib import asynccontextmanager
15
 
16
  from mcp.shared.message import SessionMessage
17
  from mcp.types import (
 
26
  logger = logging.getLogger(__name__)
27
 
28
 
29
+ class RobustHFMCPClient:
30
+ """Robust client for interacting with Hugging Face MCP endpoint optimized for Spaces."""
31
 
32
+ def __init__(self, hf_token: str, timeout: int = 120):
33
  """
34
+ Initialize the Robust Hugging Face MCP client.
35
 
36
  Args:
37
  hf_token: Hugging Face API token
38
+ timeout: Timeout in seconds for HTTP requests
39
  """
40
  self.hf_token = hf_token
41
  self.url = "https://huggingface.co/mcp"
42
  self.headers = {
43
  "Authorization": f"Bearer {hf_token}",
44
+ "User-Agent": "robust-hf-mcp-client/2.0.0",
45
+ "Accept": "application/json, text/event-stream",
46
+ "Content-Type": "application/json"
47
  }
48
  self.timeout = timedelta(seconds=timeout)
49
+ self.sse_read_timeout = timedelta(seconds=timeout * 2)
50
  self.request_id_counter = 0
51
 
52
  def _get_next_request_id(self) -> int:
53
  """Get the next request ID."""
54
  self.request_id_counter += 1
55
  return self.request_id_counter
56
+
57
+ async def _execute_single_request_session(
58
  self,
59
  method: str,
60
+ params: Optional[Dict[str, Any]] = None
 
61
  ) -> Any:
62
  """
63
+ Execute a complete MCP session for a single request.
64
+ This avoids TaskGroup issues by handling everything in sequence.
 
 
 
 
 
 
 
65
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  request_id = self._get_next_request_id()
67
 
68
+ # Create the main request
69
+ main_request = JSONRPCRequest(
70
  jsonrpc="2.0",
71
  id=request_id,
72
  method=method,
73
  params=params
74
  )
75
 
 
 
 
76
  async with streamablehttp_client(
77
  url=self.url,
78
  headers=self.headers,
79
  timeout=self.timeout,
80
  sse_read_timeout=self.sse_read_timeout,
81
+ terminate_on_close=False # Avoid TaskGroup cleanup issues
82
  ) as (read_stream, write_stream, get_session_id):
83
 
84
+ # Step 1: Initialize the session
85
+ logger.info("Starting MCP session initialization...")
86
+ await self._initialize_session(read_stream, write_stream)
87
+
88
+ # Step 2: Send the main request
89
+ logger.info(f"Sending main request: {method}")
90
+ main_message = JSONRPCMessage(main_request)
91
+ main_session_message = SessionMessage(main_message)
92
+ await write_stream.send(main_session_message)
93
+
94
+ # Step 3: Wait for the response
95
+ logger.info("Waiting for main request response...")
96
+ response = await self._wait_for_response(read_stream, request_id, timeout=90)
97
+
98
+ return response
99
+
100
+ async def _initialize_session(self, read_stream, write_stream) -> None:
101
+ """Initialize the MCP session with proper handshake."""
102
+ init_request_id = self._get_next_request_id()
103
+
104
+ # Send initialize request
105
+ init_request = JSONRPCRequest(
106
+ jsonrpc="2.0",
107
+ id=init_request_id,
108
+ method="initialize",
109
+ params={
110
+ "protocolVersion": "2024-11-05",
111
+ "capabilities": {
112
+ "tools": {},
113
+ "resources": {},
114
+ "prompts": {}
115
+ },
116
+ "clientInfo": {
117
+ "name": "robust-hf-mcp-client",
118
+ "version": "2.0.0"
119
+ }
120
+ }
121
+ )
122
+
123
+ init_message = JSONRPCMessage(init_request)
124
+ init_session_message = SessionMessage(init_message)
125
+
126
+ await write_stream.send(init_session_message)
127
+
128
+ # Wait for initialization response
129
+ init_response = await self._wait_for_response(read_stream, init_request_id, timeout=60)
130
+ logger.info("MCP session initialized successfully")
131
+
132
+ # Send initialized notification
133
+ initialized_notification = JSONRPCNotification(
134
+ jsonrpc="2.0",
135
+ method="notifications/initialized"
136
+ )
137
+
138
+ init_notif_message = JSONRPCMessage(initialized_notification)
139
+ init_notif_session_message = SessionMessage(init_notif_message)
140
+
141
+ await write_stream.send(init_notif_session_message)
142
+
143
+ # Give the server time to process the notification
144
+ await asyncio.sleep(1.0)
145
+
146
+ async def _wait_for_response(
147
+ self,
148
+ read_stream,
149
+ expected_id: int,
150
+ timeout: int = 60
151
+ ) -> Any:
152
+ """
153
+ Wait for a specific response by ID with timeout handling.
154
+ """
155
+ start_time = asyncio.get_event_loop().time()
156
+
157
+ while True:
158
+ current_time = asyncio.get_event_loop().time()
159
+ if current_time - start_time > timeout:
160
+ raise asyncio.TimeoutError(f"Timeout waiting for response to request {expected_id}")
161
+
162
  try:
163
+ # Use a shorter timeout for each receive to avoid hanging
164
+ response = await asyncio.wait_for(
165
+ read_stream.receive(),
166
+ timeout=10.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  )
168
 
169
+ if isinstance(response, Exception):
170
+ logger.error(f"Received exception in stream: {response}")
171
+ raise response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
+ if isinstance(response, SessionMessage):
174
+ msg_root = response.message.root
175
+
176
+ if isinstance(msg_root, JSONRPCResponse) and msg_root.id == expected_id:
177
+ logger.info(f"Received successful response for request {expected_id}")
178
+ return msg_root.result
179
+
180
+ elif isinstance(msg_root, JSONRPCError) and msg_root.id == expected_id:
181
+ error_msg = f"Server error for request {expected_id}: {msg_root.error}"
182
+ logger.error(error_msg)
183
+ raise Exception(error_msg)
184
 
185
+ else:
186
+ # Log unexpected messages but continue waiting
187
+ logger.debug(f"Received unexpected message type: {type(msg_root)} with ID: {getattr(msg_root, 'id', 'N/A')}")
188
+ continue
189
+
190
+ except asyncio.TimeoutError:
191
+ # Continue the outer loop to check the overall timeout
192
+ logger.debug("Receive timeout, continuing to wait...")
193
+ continue
194
  except Exception as e:
195
+ if "ClosedResourceError" in str(type(e)) or "StreamClosed" in str(e):
196
+ raise Exception("Connection closed while waiting for response")
197
+ logger.error(f"Error while waiting for response: {e}")
198
  raise
199
+
 
 
 
 
 
 
200
  async def get_all_tools(self) -> List[Dict[str, Any]]:
201
  """
202
  Get all available tools from the Hugging Face MCP endpoint.
 
206
  """
207
  try:
208
  logger.info("Fetching all available tools from Hugging Face MCP")
209
+ result = await self._execute_single_request_session("tools/list")
210
 
211
  if isinstance(result, dict) and "tools" in result:
212
  tools = result["tools"]
213
+ logger.info(f"Successfully fetched {len(tools)} tools")
214
  return tools
215
  else:
216
+ logger.warning(f"Unexpected response format for tools/list: {result}")
217
  return []
218
 
219
  except Exception as e:
220
  logger.error(f"Failed to get tools: {e}")
221
  raise
222
+
223
  async def call_tool(self, tool_name: str, args: Dict[str, Any]) -> Any:
224
  """
225
  Call a specific tool with the given arguments.
 
239
  "arguments": args
240
  }
241
 
242
+ result = await self._execute_single_request_session("tools/call", params)
243
  logger.info(f"Tool '{tool_name}' executed successfully")
244
  return result
245
 
 
248
  raise
249
 
250
 
251
+ class SimplifiedHFMCPClient:
252
+ """Ultra-simplified client that avoids all TaskGroup usage."""
253
+
254
+ def __init__(self, hf_token: str, timeout: int = 90):
255
+ self.hf_token = hf_token
256
+ self.timeout = timeout
257
+ self.headers = {
258
+ "Authorization": f"Bearer {hf_token}",
259
+ "User-Agent": "simplified-hf-mcp-client/1.0.0"
260
+ }
261
+ self.request_counter = 0
262
+
263
+ def _next_id(self) -> int:
264
+ self.request_counter += 1
265
+ return self.request_counter
266
+
267
+ async def _simple_mcp_call(self, method: str, params: Optional[Dict[str, Any]] = None) -> Any:
268
+ """Make a simple MCP call without complex async patterns."""
269
+
270
+ async with streamablehttp_client(
271
+ url="https://huggingface.co/mcp",
272
+ headers=self.headers,
273
+ timeout=timedelta(seconds=self.timeout),
274
+ sse_read_timeout=timedelta(seconds=self.timeout * 2),
275
+ terminate_on_close=False
276
+ ) as (read_stream, write_stream, get_session_id):
277
+
278
+ responses = {}
279
+
280
+ # Simple message handler
281
+ async def collect_responses():
282
+ try:
283
+ async for message in read_stream:
284
+ if isinstance(message, Exception):
285
+ responses['error'] = message
286
+ break
287
+ elif isinstance(message, SessionMessage):
288
+ msg_root = message.message.root
289
+ if hasattr(msg_root, 'id') and msg_root.id is not None:
290
+ responses[msg_root.id] = msg_root
291
+ except Exception as e:
292
+ responses['error'] = e
293
+
294
+ # Start response collector
295
+ collector_task = asyncio.create_task(collect_responses())
296
+
297
+ try:
298
+ # Step 1: Initialize
299
+ init_id = self._next_id()
300
+ init_req = JSONRPCRequest(
301
+ jsonrpc="2.0",
302
+ id=init_id,
303
+ method="initialize",
304
+ params={
305
+ "protocolVersion": "2024-11-05",
306
+ "capabilities": {"tools": {}},
307
+ "clientInfo": {"name": "simple-hf-mcp", "version": "1.0.0"}
308
+ }
309
+ )
310
+
311
+ await write_stream.send(SessionMessage(JSONRPCMessage(init_req)))
312
+
313
+ # Wait for init response
314
+ for _ in range(300): # 30 seconds max
315
+ if init_id in responses:
316
+ break
317
+ if 'error' in responses:
318
+ raise responses['error']
319
+ await asyncio.sleep(0.1)
320
+
321
+ if init_id not in responses:
322
+ raise Exception("Initialization timeout")
323
+
324
+ # Step 2: Send initialized notification
325
+ notif = JSONRPCNotification(
326
+ jsonrpc="2.0",
327
+ method="notifications/initialized"
328
+ )
329
+ await write_stream.send(SessionMessage(JSONRPCMessage(notif)))
330
+ await asyncio.sleep(0.5)
331
+
332
+ # Step 3: Send main request
333
+ main_id = self._next_id()
334
+ main_req = JSONRPCRequest(
335
+ jsonrpc="2.0",
336
+ id=main_id,
337
+ method=method,
338
+ params=params
339
+ )
340
+
341
+ await write_stream.send(SessionMessage(JSONRPCMessage(main_req)))
342
+
343
+ # Wait for main response
344
+ for _ in range(600): # 60 seconds max
345
+ if main_id in responses:
346
+ break
347
+ if 'error' in responses:
348
+ raise responses['error']
349
+ await asyncio.sleep(0.1)
350
+
351
+ if main_id not in responses:
352
+ raise Exception("Main request timeout")
353
+
354
+ result = responses[main_id]
355
+ if isinstance(result, JSONRPCResponse):
356
+ return result.result
357
+ elif isinstance(result, JSONRPCError):
358
+ raise Exception(f"Server error: {result.error}")
359
+ else:
360
+ raise Exception(f"Unexpected response type: {type(result)}")
361
+
362
+ finally:
363
+ collector_task.cancel()
364
+ try:
365
+ await collector_task
366
+ except asyncio.CancelledError:
367
+ pass
368
+
369
+ async def get_tools(self) -> List[Dict[str, Any]]:
370
+ """Get all available tools."""
371
+ result = await self._simple_mcp_call("tools/list")
372
+ if isinstance(result, dict) and "tools" in result:
373
+ return result["tools"]
374
+ return []
375
+
376
+ async def call_tool(self, tool_name: str, args: Dict[str, Any]) -> Any:
377
+ """Call a specific tool."""
378
+ params = {
379
+ "name": tool_name,
380
+ "arguments": args
381
+ }
382
+ return await self._simple_mcp_call("tools/call", params)
383
+
384
+
385
+ # Robust convenience functions
386
+ async def get_hf_tools_robust(hf_token: str, max_retries: int = 3) -> List[Dict[str, Any]]:
387
  """
388
+ Get all available tools with multiple fallback strategies.
389
 
390
  Args:
391
  hf_token: Hugging Face API token
392
+ max_retries: Maximum retry attempts per method
393
 
394
  Returns:
395
  List of tool definitions
396
  """
397
+ last_error = None
398
+
399
+ # Strategy 1: Try the robust client
400
+ for attempt in range(max_retries):
401
+ try:
402
+ logger.info(f"Trying robust client (attempt {attempt + 1})")
403
+ client = RobustHFMCPClient(hf_token, timeout=90)
404
+ tools = await client.get_all_tools()
405
+ logger.info(f"Robust client succeeded with {len(tools)} tools")
406
+ return tools
407
+ except Exception as e:
408
+ last_error = e
409
+ logger.warning(f"Robust client attempt {attempt + 1} failed: {e}")
410
+ if attempt < max_retries - 1:
411
+ await asyncio.sleep(2 ** attempt) # Exponential backoff
412
+
413
+ # Strategy 2: Try the simplified client
414
+ for attempt in range(max_retries):
415
+ try:
416
+ logger.info(f"Trying simplified client (attempt {attempt + 1})")
417
+ client = SimplifiedHFMCPClient(hf_token, timeout=120)
418
+ tools = await client.get_tools()
419
+ logger.info(f"Simplified client succeeded with {len(tools)} tools")
420
+ return tools
421
+ except Exception as e:
422
+ last_error = e
423
+ logger.warning(f"Simplified client attempt {attempt + 1} failed: {e}")
424
+ if attempt < max_retries - 1:
425
+ await asyncio.sleep(2 ** attempt)
426
+
427
+ # If all strategies fail
428
+ raise Exception(f"All connection strategies failed. Last error: {last_error}")
429
 
430
 
431
+ async def call_hf_tool_robust(
432
+ hf_token: str,
433
+ tool_name: str,
434
+ args: Dict[str, Any],
435
+ max_retries: int = 3
436
+ ) -> Any:
437
  """
438
+ Call a specific Hugging Face MCP tool with multiple fallback strategies.
439
 
440
  Args:
441
  hf_token: Hugging Face API token
442
  tool_name: Name of the tool to call
443
  args: Arguments to pass to the tool
444
+ max_retries: Maximum retry attempts per method
445
 
446
  Returns:
447
  The tool's response
448
  """
449
+ last_error = None
450
+
451
+ # Strategy 1: Try the robust client
452
+ for attempt in range(max_retries):
453
+ try:
454
+ logger.info(f"Trying robust client for tool call (attempt {attempt + 1})")
455
+ client = RobustHFMCPClient(hf_token, timeout=120)
456
+ result = await client.call_tool(tool_name, args)
457
+ logger.info(f"Robust client tool call succeeded")
458
+ return result
459
+ except Exception as e:
460
+ last_error = e
461
+ logger.warning(f"Robust client tool call attempt {attempt + 1} failed: {e}")
462
+ if attempt < max_retries - 1:
463
+ await asyncio.sleep(2 ** attempt)
464
+
465
+ # Strategy 2: Try the simplified client
466
+ for attempt in range(max_retries):
467
+ try:
468
+ logger.info(f"Trying simplified client for tool call (attempt {attempt + 1})")
469
+ client = SimplifiedHFMCPClient(hf_token, timeout=150)
470
+ result = await client.call_tool(tool_name, args)
471
+ logger.info(f"Simplified client tool call succeeded")
472
+ return result
473
+ except Exception as e:
474
+ last_error = e
475
+ logger.warning(f"Simplified client tool call attempt {attempt + 1} failed: {e}")
476
+ if attempt < max_retries - 1:
477
+ await asyncio.sleep(2 ** attempt)
478
+
479
+ # If all strategies fail
480
+ raise Exception(f"All tool call strategies failed. Last error: {last_error}")
481
 
482
 
483
+ # Legacy compatibility functions
484
+ async def get_hf_tools(hf_token: str) -> List[Dict[str, Any]]:
485
+ """Legacy function - now uses robust implementation."""
486
+ return await get_hf_tools_robust(hf_token)
487
+
488
+
489
+ async def call_hf_tool(hf_token: str, tool_name: str, args: Dict[str, Any]) -> Any:
490
+ """Legacy function - now uses robust implementation."""
491
+ return await call_hf_tool_robust(hf_token, tool_name, args)
492
+
493
+
494
+ # Enhanced diagnostics
495
+ async def diagnose_connection_advanced(hf_token: str) -> Dict[str, Any]:
496
  """
497
+ Advanced connection diagnostics with multiple test scenarios.
498
 
499
  Args:
500
  hf_token: Hugging Face API token
501
 
502
  Returns:
503
+ Comprehensive diagnostic information
504
  """
505
  diagnostics = {
506
  "environment": "huggingface_spaces" if os.getenv("SPACE_ID") else "local",
507
  "space_id": os.getenv("SPACE_ID"),
508
+ "python_version": os.sys.version,
509
  "token_length": len(hf_token) if hf_token else 0,
510
  "has_token": bool(hf_token),
511
+ "tests": {
512
+ "basic_connection": False,
513
+ "robust_client": False,
514
+ "simplified_client": False,
515
+ "tools_fetch": False,
516
+ "tool_call_test": False
517
+ },
518
+ "errors": {},
519
+ "tool_count": 0,
520
+ "sample_tools": []
521
  }
522
 
523
+ # Test 1: Basic connection
524
  try:
 
 
 
 
 
 
 
 
525
  async with streamablehttp_client(
526
  url="https://huggingface.co/mcp",
527
+ headers={"Authorization": f"Bearer {hf_token}"},
528
+ timeout=timedelta(seconds=10),
529
+ terminate_on_close=False
530
  ) as (read_stream, write_stream, get_session_id):
531
+ diagnostics["tests"]["basic_connection"] = True
532
  logger.info("Basic connection test passed")
533
+ except Exception as e:
534
+ diagnostics["errors"]["basic_connection"] = str(e)
535
+ logger.error(f"Basic connection test failed: {e}")
536
+
537
+ # Test 2: Robust client
538
+ if diagnostics["tests"]["basic_connection"]:
539
+ try:
540
+ client = RobustHFMCPClient(hf_token, timeout=60)
541
+ tools = await client.get_all_tools()
542
+ diagnostics["tests"]["robust_client"] = True
543
+ diagnostics["tests"]["tools_fetch"] = True
544
+ diagnostics["tool_count"] = len(tools)
545
+ diagnostics["sample_tools"] = [
546
+ {"name": tool.get("name"), "description": tool.get("description", "")[:100]}
547
+ for tool in tools[:3]
548
+ ]
549
+ logger.info(f"Robust client test passed - {len(tools)} tools")
550
+ except Exception as e:
551
+ diagnostics["errors"]["robust_client"] = str(e)
552
+ logger.error(f"Robust client test failed: {e}")
553
+
554
+ # Test 3: Simplified client
555
+ if not diagnostics["tests"]["robust_client"]:
556
+ try:
557
+ client = SimplifiedHFMCPClient(hf_token, timeout=90)
558
+ tools = await client.get_tools()
559
+ diagnostics["tests"]["simplified_client"] = True
560
+ if not diagnostics["tests"]["tools_fetch"]:
561
+ diagnostics["tests"]["tools_fetch"] = True
562
  diagnostics["tool_count"] = len(tools)
563
+ diagnostics["sample_tools"] = [
564
+ {"name": tool.get("name"), "description": tool.get("description", "")[:100]}
565
+ for tool in tools[:3]
566
+ ]
567
+ logger.info(f"Simplified client test passed - {len(tools)} tools")
568
+ except Exception as e:
569
+ diagnostics["errors"]["simplified_client"] = str(e)
570
+ logger.error(f"Simplified client test failed: {e}")
571
+
572
+ # Test 4: Tool call (if we have tools)
573
+ if diagnostics["tests"]["tools_fetch"] and diagnostics["sample_tools"]:
574
+ try:
575
+ # Try to call a simple tool if available
576
+ sample_tool_name = diagnostics["sample_tools"][0]["name"]
577
+ if sample_tool_name:
578
+ # Use the working client
579
+ if diagnostics["tests"]["robust_client"]:
580
+ client = RobustHFMCPClient(hf_token, timeout=60)
581
+ else:
582
+ client = SimplifiedHFMCPClient(hf_token, timeout=90)
583
 
584
+ # Try with empty args first (many tools accept this)
585
+ try:
586
+ result = await client.call_tool(sample_tool_name, {})
587
+ diagnostics["tests"]["tool_call_test"] = True
588
+ logger.info(f"Tool call test passed with {sample_tool_name}")
589
+ except Exception as tool_error:
590
+ # Tool call failed but that might be due to wrong args
591
+ diagnostics["errors"]["tool_call_test"] = f"Tool call failed (might need args): {str(tool_error)}"
592
+ logger.warning(f"Tool call test failed: {tool_error}")
593
+
594
+ except Exception as e:
595
+ diagnostics["errors"]["tool_call_test"] = str(e)
596
+ logger.error(f"Tool call test setup failed: {e}")
597
 
598
+ return diagnostics