fast-agent-mcp 0.2.20__py3-none-any.whl → 0.2.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

@@ -41,10 +41,12 @@ SEP = "-"
41
41
  T = TypeVar("T")
42
42
  R = TypeVar("R")
43
43
 
44
+
44
45
  def create_namespaced_name(server_name: str, resource_name: str) -> str:
45
46
  """Create a namespaced resource name from server and resource names"""
46
47
  return f"{server_name}{SEP}{resource_name}"
47
48
 
49
+
48
50
  def is_namespaced_name(name: str) -> bool:
49
51
  """Check if a name is already namespaced"""
50
52
  return SEP in name
@@ -325,14 +327,14 @@ class MCPAggregator(ContextDependent):
325
327
  except Exception as e:
326
328
  logger.debug(f"Error getting capabilities for server '{server_name}': {e}")
327
329
  return None
328
-
330
+
329
331
  async def validate_server(self, server_name: str) -> bool:
330
332
  """
331
333
  Validate that a server exists in our server list.
332
-
334
+
333
335
  Args:
334
336
  server_name: Name of the server to validate
335
-
337
+
336
338
  Returns:
337
339
  True if the server exists, False otherwise
338
340
  """
@@ -340,25 +342,25 @@ class MCPAggregator(ContextDependent):
340
342
  if not valid:
341
343
  logger.debug(f"Server '{server_name}' not found")
342
344
  return valid
343
-
345
+
344
346
  async def server_supports_feature(self, server_name: str, feature: str) -> bool:
345
347
  """
346
348
  Check if a server supports a specific feature.
347
-
349
+
348
350
  Args:
349
351
  server_name: Name of the server to check
350
352
  feature: Feature to check for (e.g., "prompts", "resources")
351
-
353
+
352
354
  Returns:
353
355
  True if the server supports the feature, False otherwise
354
356
  """
355
357
  if not await self.validate_server(server_name):
356
358
  return False
357
-
359
+
358
360
  capabilities = await self.get_capabilities(server_name)
359
361
  if not capabilities:
360
362
  return False
361
-
363
+
362
364
  return getattr(capabilities, feature, False)
363
365
 
364
366
  async def list_servers(self) -> List[str]:
@@ -465,26 +467,26 @@ class MCPAggregator(ContextDependent):
465
467
  if resource_type == "tool" and name in self._namespaced_tool_map:
466
468
  namespaced_tool = self._namespaced_tool_map[name]
467
469
  return namespaced_tool.server_name, namespaced_tool.tool.name
468
-
470
+
469
471
  # Next, attempt to interpret as a namespaced name
470
472
  if is_namespaced_name(name):
471
473
  parts = name.split(SEP, 1)
472
474
  server_name, local_name = parts[0], parts[1]
473
-
475
+
474
476
  # Validate that the parsed server actually exists
475
477
  if server_name in self.server_names:
476
478
  return server_name, local_name
477
-
479
+
478
480
  # If the server name doesn't exist, it might be a tool with a hyphen in its name
479
481
  # Fall through to the next checks
480
-
482
+
481
483
  # For tools, search all servers for the tool by exact name match
482
484
  if resource_type == "tool":
483
485
  for server_name, tools in self._server_to_tool_map.items():
484
486
  for namespaced_tool in tools:
485
487
  if namespaced_tool.tool.name == name:
486
488
  return server_name, name
487
-
489
+
488
490
  # For all other resource types, use the first server
489
491
  return (self.server_names[0] if self.server_names else None, name)
490
492
 
@@ -524,7 +526,10 @@ class MCPAggregator(ContextDependent):
524
526
  operation_type="tool",
525
527
  operation_name=local_tool_name,
526
528
  method_name="call_tool",
527
- method_args={"name": local_tool_name, "arguments": arguments},
529
+ method_args={
530
+ "name": local_tool_name,
531
+ "arguments": arguments,
532
+ },
528
533
  error_factory=lambda msg: CallToolResult(
529
534
  isError=True, content=[TextContent(type="text", text=msg)]
530
535
  ),
@@ -558,7 +563,7 @@ class MCPAggregator(ContextDependent):
558
563
  elif is_namespaced_name(prompt_name):
559
564
  parts = prompt_name.split(SEP, 1)
560
565
  potential_server = parts[0]
561
-
566
+
562
567
  # Only treat as namespaced if the server part is valid
563
568
  if potential_server in self.server_names:
564
569
  server_name = potential_server
@@ -570,7 +575,7 @@ class MCPAggregator(ContextDependent):
570
575
  else:
571
576
  local_prompt_name = prompt_name
572
577
  # We'll search all servers below
573
-
578
+
574
579
  # If we have a specific server to check
575
580
  if server_name:
576
581
  if not await self.validate_server(server_name):
@@ -579,7 +584,7 @@ class MCPAggregator(ContextDependent):
579
584
  description=f"Error: Server '{server_name}' not found",
580
585
  messages=[],
581
586
  )
582
-
587
+
583
588
  # Check if server supports prompts
584
589
  if not await self.server_supports_feature(server_name, "prompts"):
585
590
  logger.debug(f"Server '{server_name}' does not support prompts")
@@ -15,6 +15,7 @@ from typing import (
15
15
 
16
16
  from anyio import Event, Lock, create_task_group
17
17
  from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
18
+ from httpx import HTTPStatusError
18
19
  from mcp import ClientSession
19
20
  from mcp.client.sse import sse_client
20
21
  from mcp.client.stdio import (
@@ -162,14 +163,26 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
162
163
  transport_context = server_conn._transport_context_factory()
163
164
 
164
165
  async with transport_context as (read_stream, write_stream):
165
- # try:
166
166
  server_conn.create_session(read_stream, write_stream)
167
167
 
168
168
  async with server_conn.session:
169
169
  await server_conn.initialize_session()
170
-
171
170
  await server_conn.wait_for_shutdown_request()
172
171
 
172
+ except HTTPStatusError as http_exc:
173
+ logger.error(
174
+ f"{server_name}: Lifecycle task encountered HTTP error: {http_exc}",
175
+ exc_info=True,
176
+ data={
177
+ "progress_action": ProgressAction.FATAL_ERROR,
178
+ "server_name": server_name,
179
+ },
180
+ )
181
+ server_conn._error_occurred = True
182
+ server_conn._error_message = f"HTTP Error: {http_exc.response.status_code} {http_exc.response.reason_phrase} for URL: {http_exc.request.url}"
183
+ server_conn._initialized_event.set()
184
+ # No raise - let get_server handle it with a friendly message
185
+
173
186
  except Exception as exc:
174
187
  logger.error(
175
188
  f"{server_name}: Lifecycle task encountered an error: {exc}",
@@ -180,7 +193,27 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
180
193
  },
181
194
  )
182
195
  server_conn._error_occurred = True
183
- server_conn._error_message = traceback.format_exception(exc)
196
+
197
+ if "ExceptionGroup" in type(exc).__name__ and hasattr(exc, "exceptions"):
198
+ # Handle ExceptionGroup better by extracting the actual errors
199
+ error_messages = []
200
+ for subexc in exc.exceptions:
201
+ if isinstance(subexc, HTTPStatusError):
202
+ # Special handling for HTTP errors to make them more user-friendly
203
+ error_messages.append(
204
+ f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
205
+ )
206
+ else:
207
+ error_messages.append(f"Error: {type(subexc).__name__}: {subexc}")
208
+ if hasattr(subexc, "__cause__") and subexc.__cause__:
209
+ error_messages.append(
210
+ f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
211
+ )
212
+ server_conn._error_message = error_messages
213
+ else:
214
+ # For regular exceptions, keep the traceback but format it more cleanly
215
+ server_conn._error_message = traceback.format_exception(exc)
216
+
184
217
  # If there's an error, we should also set the event so that
185
218
  # 'get_server' won't hang
186
219
  server_conn._initialized_event.set()
@@ -277,6 +310,7 @@ class MCPConnectionManager(ContextDependent):
277
310
  config.headers,
278
311
  sse_read_timeout=config.read_transport_sse_timeout_seconds,
279
312
  )
313
+
280
314
  else:
281
315
  raise ValueError(f"Unsupported transport: {config.transport}")
282
316
 
@@ -333,9 +367,17 @@ class MCPConnectionManager(ContextDependent):
333
367
  # Check if the server is healthy after initialization
334
368
  if not server_conn.is_healthy():
335
369
  error_msg = server_conn._error_message or "Unknown error"
370
+
371
+ # Format the error message for better display
372
+ if isinstance(error_msg, list):
373
+ # Join the list with newlines for better readability
374
+ formatted_error = "\n".join(error_msg)
375
+ else:
376
+ formatted_error = str(error_msg)
377
+
336
378
  raise ServerInitializationError(
337
379
  f"MCP Server: '{server_name}': Failed to initialize - see details. Check fastagent.config.yaml?",
338
- error_msg,
380
+ formatted_error,
339
381
  )
340
382
 
341
383
  return server_conn
@@ -82,6 +82,7 @@ class PromptConfig(PromptMetadata):
82
82
  http_timeout: float = 10.0
83
83
  transport: str = "stdio"
84
84
  port: int = 8000
85
+ host: str = "0.0.0.0"
85
86
 
86
87
 
87
88
  # We'll maintain registries of all exposed resources and prompts
@@ -344,6 +345,12 @@ def parse_args():
344
345
  default=8000,
345
346
  help="Port to use for SSE transport (default: 8000)",
346
347
  )
348
+ parser.add_argument(
349
+ "--host",
350
+ type=str,
351
+ default="0.0.0.0",
352
+ help="Host to bind to for SSE transport (default: 0.0.0.0)",
353
+ )
347
354
  parser.add_argument(
348
355
  "--test", type=str, help="Test a specific prompt without starting the server"
349
356
  )
@@ -380,6 +387,7 @@ def initialize_config(args) -> PromptConfig:
380
387
  http_timeout=args.http_timeout,
381
388
  transport=args.transport,
382
389
  port=args.port,
390
+ host=args.host,
383
391
  )
384
392
 
385
393
 
@@ -497,7 +505,10 @@ async def async_main() -> int:
497
505
  if config.transport == "stdio":
498
506
  await mcp.run_stdio_async()
499
507
  else: # sse
500
- # TODO update to 2025-03-26 specification and test config.
508
+ # Set the host and port in settings before running the server
509
+ mcp.settings.host = config.host
510
+ mcp.settings.port = config.port
511
+ logger.info(f"Starting SSE server on {config.host}:{config.port}")
501
512
  await mcp.run_sse_async()
502
513
  return 0
503
514