Y Phung Nguyen commited on
Commit
6ab08df
·
1 Parent(s): 2f3ac98

Add timeout mcp

Browse files
Files changed (2) hide show
  1. agent.py +19 -2
  2. client.py +39 -7
agent.py CHANGED
@@ -169,10 +169,12 @@ async def list_tools() -> list[Tool]:
169
  @app.call_tool()
170
  async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
171
  """Handle tool calls"""
 
172
  if name == "generate_content":
173
  try:
174
  user_prompt = arguments.get("user_prompt")
175
  if not user_prompt:
 
176
  return [TextContent(type="text", text="Error: user_prompt is required")]
177
 
178
  system_prompt = arguments.get("system_prompt")
@@ -221,6 +223,10 @@ async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageC
221
  "max_output_tokens": GEMINI_MAX_OUTPUT_TOKENS
222
  }
223
 
 
 
 
 
224
  # Use asyncio.to_thread to make the blocking call async
225
  # The API accepts contents as a list and config as a separate parameter
226
  def generate_sync():
@@ -230,7 +236,11 @@ async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageC
230
  config=generation_config,
231
  )
232
 
233
- response = await asyncio.to_thread(generate_sync)
 
 
 
 
234
 
235
  # Extract text from response
236
  if response and hasattr(response, 'text') and response.text:
@@ -253,8 +263,15 @@ async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageC
253
  logger.warning("Gemini returned empty response")
254
  return [TextContent(type="text", text="Error: No response from Gemini")]
255
 
 
 
 
 
 
256
  except Exception as e:
257
- logger.error(f"Error generating content: {e}")
 
 
258
  return [TextContent(type="text", text=f"Error: {str(e)}")]
259
 
260
  except Exception as e:
 
169
  @app.call_tool()
170
  async def call_tool(name: str, arguments: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
171
  """Handle tool calls"""
172
+ logger.info(f"🔵 MCP tool call received: {name}")
173
  if name == "generate_content":
174
  try:
175
  user_prompt = arguments.get("user_prompt")
176
  if not user_prompt:
177
+ logger.error("❌ user_prompt is required but missing")
178
  return [TextContent(type="text", text="Error: user_prompt is required")]
179
 
180
  system_prompt = arguments.get("system_prompt")
 
223
  "max_output_tokens": GEMINI_MAX_OUTPUT_TOKENS
224
  }
225
 
226
+ # Convert timeout from milliseconds to seconds, cap at 100s to stay under 120s function limit
227
+ timeout_seconds = min(GEMINI_TIMEOUT / 1000.0, 100.0)
228
+ logger.info(f"🔵 Calling Gemini API with model={model}, timeout={timeout_seconds}s...")
229
+
230
  # Use asyncio.to_thread to make the blocking call async
231
  # The API accepts contents as a list and config as a separate parameter
232
  def generate_sync():
 
236
  config=generation_config,
237
  )
238
 
239
+ response = await asyncio.wait_for(
240
+ asyncio.to_thread(generate_sync),
241
+ timeout=timeout_seconds
242
+ )
243
+ logger.info(f"✅ Gemini API call completed successfully")
244
 
245
  # Extract text from response
246
  if response and hasattr(response, 'text') and response.text:
 
263
  logger.warning("Gemini returned empty response")
264
  return [TextContent(type="text", text="Error: No response from Gemini")]
265
 
266
+ except asyncio.TimeoutError:
267
+ timeout_seconds = min(GEMINI_TIMEOUT / 1000.0, 100.0)
268
+ error_msg = f"Gemini API call timed out after {timeout_seconds}s"
269
+ logger.error(f"❌ {error_msg}")
270
+ return [TextContent(type="text", text=f"Error: {error_msg}")]
271
  except Exception as e:
272
+ logger.error(f"Error generating content: {type(e).__name__}: {e}")
273
+ import traceback
274
+ logger.debug(f"Full traceback: {traceback.format_exc()}")
275
  return [TextContent(type="text", text=f"Error: {str(e)}")]
276
 
277
  except Exception as e:
client.py CHANGED
@@ -44,6 +44,7 @@ async def get_mcp_session():
44
 
45
  # Reuse existing session if available
46
  if config.global_mcp_session is not None:
 
47
  return config.global_mcp_session
48
 
49
  try:
@@ -198,23 +199,27 @@ async def call_agent(user_prompt: str, system_prompt: str = None, files: list =
198
  try:
199
  session = await get_mcp_session()
200
  if session is None:
201
- logger.warning("Failed to get MCP session for Gemini call - check GEMINI_API_KEY and agent.py")
202
  # Invalidate session to force retry on next call
203
  config.global_mcp_session = None
204
  config.global_mcp_stdio_ctx = None
205
  return ""
206
 
 
 
207
  tools = await get_cached_mcp_tools()
208
  if not tools:
209
  logger.info("MCP tools cache empty, refreshing...")
210
  tools = await get_cached_mcp_tools(force_refresh=True)
211
  if not tools:
212
- logger.error("Unable to obtain MCP tool catalog for Gemini calls")
213
  # Invalidate session to force retry on next call
214
  config.global_mcp_session = None
215
  config.global_mcp_stdio_ctx = None
216
  return ""
217
 
 
 
218
  generate_tool = None
219
  for tool in tools:
220
  if tool.name == "generate_content" or "generate_content" in tool.name.lower():
@@ -239,24 +244,51 @@ async def call_agent(user_prompt: str, system_prompt: str = None, files: list =
239
  if temperature is not None:
240
  arguments["temperature"] = temperature
241
 
242
- logger.debug(f"Calling MCP tool {generate_tool.name} with arguments: {list(arguments.keys())}")
243
- result = await session.call_tool(generate_tool.name, arguments=arguments)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
  if hasattr(result, 'content') and result.content:
246
  for item in result.content:
247
  if hasattr(item, 'text'):
248
  response_text = item.text.strip()
249
  if response_text:
250
- logger.debug(f"Gemini MCP returned {len(response_text)} chars")
251
  return response_text
252
  logger.warning("⚠️ Gemini MCP returned empty or invalid result")
253
  return ""
254
  except Exception as e:
255
  error_type = type(e).__name__
256
  error_msg = str(e)
257
- logger.error(f"Gemini MCP call error: {error_type}: {error_msg}")
258
  import traceback
259
- logger.debug(f"Full traceback: {traceback.format_exc()}")
260
  # Invalidate session on error to force retry
261
  config.global_mcp_session = None
262
  config.global_mcp_stdio_ctx = None
 
44
 
45
  # Reuse existing session if available
46
  if config.global_mcp_session is not None:
47
+ logger.debug("Reusing existing MCP session")
48
  return config.global_mcp_session
49
 
50
  try:
 
199
  try:
200
  session = await get_mcp_session()
201
  if session is None:
202
+ logger.error("Failed to get MCP session for Gemini call - check GEMINI_API_KEY and agent.py")
203
  # Invalidate session to force retry on next call
204
  config.global_mcp_session = None
205
  config.global_mcp_stdio_ctx = None
206
  return ""
207
 
208
+ logger.debug(f"MCP session obtained: {type(session).__name__}")
209
+
210
  tools = await get_cached_mcp_tools()
211
  if not tools:
212
  logger.info("MCP tools cache empty, refreshing...")
213
  tools = await get_cached_mcp_tools(force_refresh=True)
214
  if not tools:
215
+ logger.error("Unable to obtain MCP tool catalog for Gemini calls")
216
  # Invalidate session to force retry on next call
217
  config.global_mcp_session = None
218
  config.global_mcp_stdio_ctx = None
219
  return ""
220
 
221
+ logger.debug(f"Found {len(tools)} MCP tools available")
222
+
223
  generate_tool = None
224
  for tool in tools:
225
  if tool.name == "generate_content" or "generate_content" in tool.name.lower():
 
244
  if temperature is not None:
245
  arguments["temperature"] = temperature
246
 
247
+ logger.info(f"🔵 Calling MCP tool '{generate_tool.name}' with model={model or 'default'}, temperature={temperature}")
248
+ logger.debug(f"MCP tool arguments keys: {list(arguments.keys())}")
249
+ logger.debug(f"User prompt length: {len(user_prompt)} chars")
250
+
251
+ # Add timeout to prevent hanging (max 20s to stay under 120s function limit)
252
+ # Also add a shorter timeout for the initial connection check
253
+ try:
254
+ logger.debug("Starting MCP tool call with 20s timeout...")
255
+ result = await asyncio.wait_for(
256
+ session.call_tool(generate_tool.name, arguments=arguments),
257
+ timeout=20.0
258
+ )
259
+ logger.info(f"✅ MCP tool call completed successfully")
260
+ except asyncio.TimeoutError:
261
+ logger.error(f"❌ MCP tool call timed out after 20s - this exceeds the function time limit")
262
+ logger.error(f" This suggests the MCP server (agent.py) is not responding or the Gemini API call is hanging")
263
+ logger.error(f" Check if agent.py process is still running and responsive")
264
+ # Invalidate session on timeout to force retry
265
+ config.global_mcp_session = None
266
+ config.global_mcp_stdio_ctx = None
267
+ return ""
268
+ except Exception as call_error:
269
+ logger.error(f"❌ MCP tool call failed with exception: {type(call_error).__name__}: {call_error}")
270
+ import traceback
271
+ logger.error(f" Traceback: {traceback.format_exc()}")
272
+ # Invalidate session on error to force retry
273
+ config.global_mcp_session = None
274
+ config.global_mcp_stdio_ctx = None
275
+ raise # Re-raise to be caught by outer exception handler
276
 
277
  if hasattr(result, 'content') and result.content:
278
  for item in result.content:
279
  if hasattr(item, 'text'):
280
  response_text = item.text.strip()
281
  if response_text:
282
+ logger.info(f"Gemini MCP returned {len(response_text)} chars")
283
  return response_text
284
  logger.warning("⚠️ Gemini MCP returned empty or invalid result")
285
  return ""
286
  except Exception as e:
287
  error_type = type(e).__name__
288
  error_msg = str(e)
289
+ logger.error(f"Gemini MCP call error: {error_type}: {error_msg}")
290
  import traceback
291
+ logger.error(f"Full traceback: {traceback.format_exc()}")
292
  # Invalidate session on error to force retry
293
  config.global_mcp_session = None
294
  config.global_mcp_stdio_ctx = None