fast-agent-mcp 0.2.17__py3-none-any.whl → 0.2.19__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.
mcp_agent/context.py CHANGED
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Any, Optional, Union
9
9
  from mcp import ServerSession
10
10
  from opentelemetry import trace
11
11
  from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
12
+ from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
13
+ from opentelemetry.instrumentation.openai import OpenAIInstrumentor
12
14
  from opentelemetry.propagate import set_global_textmap
13
15
  from opentelemetry.sdk.resources import Resource
14
16
  from opentelemetry.sdk.trace import TracerProvider
@@ -51,7 +53,7 @@ class Context(BaseModel):
51
53
  server_registry: Optional[ServerRegistry] = None
52
54
  task_registry: Optional[ActivityRegistry] = None
53
55
 
54
- tracer: Optional[trace.Tracer] = None
56
+ tracer: trace.Tracer | None = None
55
57
 
56
58
  model_config = ConfigDict(
57
59
  extra="allow",
@@ -63,19 +65,19 @@ async def configure_otel(config: "Settings") -> None:
63
65
  """
64
66
  Configure OpenTelemetry based on the application config.
65
67
  """
66
- if not config.otel.enabled:
67
- return
68
-
69
- # Check if a provider is already set to avoid re-initialization
70
- if trace.get_tracer_provider().__class__.__name__ != "NoOpTracerProvider":
68
+ if not config.otel or not config.otel.enabled:
71
69
  return
72
70
 
73
71
  # Set up global textmap propagator first
74
72
  set_global_textmap(TraceContextTextMapPropagator())
75
73
 
76
74
  service_name = config.otel.service_name
77
- service_instance_id = config.otel.service_instance_id
78
- service_version = config.otel.service_version
75
+ from importlib.metadata import version
76
+
77
+ try:
78
+ app_version = version("fast-agent-mcp")
79
+ except: # noqa: E722
80
+ app_version = "unknown"
79
81
 
80
82
  # Create resource identifying this service
81
83
  resource = Resource.create(
@@ -83,8 +85,7 @@ async def configure_otel(config: "Settings") -> None:
83
85
  key: value
84
86
  for key, value in {
85
87
  "service.name": service_name,
86
- "service.instance.id": service_instance_id,
87
- "service.version": service_version,
88
+ "service.version": app_version,
88
89
  }.items()
89
90
  if value is not None
90
91
  }
@@ -107,6 +108,8 @@ async def configure_otel(config: "Settings") -> None:
107
108
 
108
109
  # Set as global tracer provider
109
110
  trace.set_tracer_provider(tracer_provider)
111
+ AnthropicInstrumentor().instrument()
112
+ OpenAIInstrumentor().instrument()
110
113
 
111
114
 
112
115
  async def configure_logger(config: "Settings") -> None:
@@ -289,14 +289,19 @@ async def get_enhanced_input(
289
289
  # Return a dictionary with select_prompt action instead of a string
290
290
  # This way it will match what the command handler expects
291
291
  return {"select_prompt": True, "prompt_name": None}
292
- elif cmd == "prompt" and len(cmd_parts) > 1:
293
- # Direct prompt selection with name or number
294
- prompt_arg = cmd_parts[1].strip()
295
- # Check if it's a number (use as index) or a name (use directly)
296
- if prompt_arg.isdigit():
297
- return {"select_prompt": True, "prompt_index": int(prompt_arg)}
292
+ elif cmd == "prompt":
293
+ # Handle /prompt with no arguments the same way as /prompts
294
+ if len(cmd_parts) > 1:
295
+ # Direct prompt selection with name or number
296
+ prompt_arg = cmd_parts[1].strip()
297
+ # Check if it's a number (use as index) or a name (use directly)
298
+ if prompt_arg.isdigit():
299
+ return {"select_prompt": True, "prompt_index": int(prompt_arg)}
300
+ else:
301
+ return f"SELECT_PROMPT:{prompt_arg}"
298
302
  else:
299
- return f"SELECT_PROMPT:{prompt_arg}"
303
+ # If /prompt is used without arguments, treat it the same as /prompts
304
+ return {"select_prompt": True, "prompt_name": None}
300
305
  elif cmd == "exit":
301
306
  return "EXIT"
302
307
  elif cmd.lower() == "stop":
@@ -9,9 +9,11 @@ import asyncio
9
9
  import sys
10
10
  from contextlib import asynccontextmanager
11
11
  from importlib.metadata import version as get_version
12
- from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypeVar
13
14
 
14
15
  import yaml
16
+ from opentelemetry import trace
15
17
 
16
18
  from mcp_agent import config
17
19
  from mcp_agent.app import MCPApp
@@ -54,9 +56,11 @@ from mcp_agent.core.validation import (
54
56
  validate_workflow_references,
55
57
  )
56
58
  from mcp_agent.logging.logger import get_logger
59
+ from mcp_agent.mcp.prompts.prompt_load import load_prompt_multipart
57
60
 
58
61
  if TYPE_CHECKING:
59
62
  from mcp_agent.agents.agent import Agent
63
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
60
64
 
61
65
  F = TypeVar("F", bound=Callable[..., Any]) # For decorated functions
62
66
  logger = get_logger(__name__)
@@ -73,77 +77,97 @@ class FastAgent:
73
77
  name: str,
74
78
  config_path: str | None = None,
75
79
  ignore_unknown_args: bool = False,
80
+ parse_cli_args: bool = True, # Add new parameter with default True
76
81
  ) -> None:
77
82
  """
78
- Initialize the DirectFastAgent application.
83
+ Initialize the fast-agent application.
79
84
 
80
85
  Args:
81
86
  name: Name of the application
82
87
  config_path: Optional path to config file
83
88
  ignore_unknown_args: Whether to ignore unknown command line arguments
89
+ when parse_cli_args is True.
90
+ parse_cli_args: If True, parse command line arguments using argparse.
91
+ Set to False when embedding FastAgent in another framework
92
+ (like FastAPI/Uvicorn) that handles its own arguments.
84
93
  """
85
- # Setup command line argument parsing
86
- parser = argparse.ArgumentParser(description="DirectFastAgent Application")
87
- parser.add_argument(
88
- "--model",
89
- help="Override the default model for all agents",
90
- )
91
- parser.add_argument(
92
- "--agent",
93
- help="Specify the agent to send a message to (used with --message)",
94
- )
95
- parser.add_argument(
96
- "-m",
97
- "--message",
98
- help="Message to send to the specified agent (requires --agent)",
99
- )
100
- parser.add_argument(
101
- "--quiet",
102
- action="store_true",
103
- help="Disable progress display, tool and message logging for cleaner output",
104
- )
105
- parser.add_argument(
106
- "--version",
107
- action="store_true",
108
- help="Show version and exit",
109
- )
110
- parser.add_argument(
111
- "--server",
112
- action="store_true",
113
- help="Run as an MCP server",
114
- )
115
- parser.add_argument(
116
- "--transport",
117
- choices=["sse", "stdio"],
118
- default="sse",
119
- help="Transport protocol to use when running as a server (sse or stdio)",
120
- )
121
- parser.add_argument(
122
- "--port",
123
- type=int,
124
- default=8000,
125
- help="Port to use when running as a server with SSE transport",
126
- )
127
- parser.add_argument(
128
- "--host",
129
- default="0.0.0.0",
130
- help="Host address to bind to when running as a server with SSE transport",
131
- )
132
-
133
- if ignore_unknown_args:
134
- known_args, _ = parser.parse_known_args()
135
- self.args = known_args
136
- else:
137
- self.args = parser.parse_args()
94
+ self.args = argparse.Namespace() # Initialize args always
95
+
96
+ # --- Wrap argument parsing logic ---
97
+ if parse_cli_args:
98
+ # Setup command line argument parsing
99
+ parser = argparse.ArgumentParser(description="DirectFastAgent Application")
100
+ parser.add_argument(
101
+ "--model",
102
+ help="Override the default model for all agents",
103
+ )
104
+ parser.add_argument(
105
+ "--agent",
106
+ default="default",
107
+ help="Specify the agent to send a message to (used with --message)",
108
+ )
109
+ parser.add_argument(
110
+ "-m",
111
+ "--message",
112
+ help="Message to send to the specified agent",
113
+ )
114
+ parser.add_argument(
115
+ "-p", "--prompt-file", help="Path to a prompt file to use (either text or JSON)"
116
+ )
117
+ parser.add_argument(
118
+ "--quiet",
119
+ action="store_true",
120
+ help="Disable progress display, tool and message logging for cleaner output",
121
+ )
122
+ parser.add_argument(
123
+ "--version",
124
+ action="store_true",
125
+ help="Show version and exit",
126
+ )
127
+ parser.add_argument(
128
+ "--server",
129
+ action="store_true",
130
+ help="Run as an MCP server",
131
+ )
132
+ parser.add_argument(
133
+ "--transport",
134
+ choices=["sse", "stdio"],
135
+ default="sse",
136
+ help="Transport protocol to use when running as a server (sse or stdio)",
137
+ )
138
+ parser.add_argument(
139
+ "--port",
140
+ type=int,
141
+ default=8000,
142
+ help="Port to use when running as a server with SSE transport",
143
+ )
144
+ parser.add_argument(
145
+ "--host",
146
+ default="0.0.0.0",
147
+ help="Host address to bind to when running as a server with SSE transport",
148
+ )
138
149
 
139
- # Handle version flag
140
- if self.args.version:
141
- try:
142
- app_version = get_version("fast-agent-mcp")
143
- except: # noqa: E722
144
- app_version = "unknown"
145
- print(f"fast-agent-mcp v{app_version}")
146
- sys.exit(0)
150
+ if ignore_unknown_args:
151
+ known_args, _ = parser.parse_known_args()
152
+ self.args = known_args
153
+ else:
154
+ # Use parse_known_args here too, to avoid crashing on uvicorn args etc.
155
+ # even if ignore_unknown_args is False, we only care about *our* args.
156
+ known_args, unknown = parser.parse_known_args()
157
+ self.args = known_args
158
+ # Optionally, warn about unknown args if not ignoring?
159
+ # if unknown and not ignore_unknown_args:
160
+ # logger.warning(f"Ignoring unknown command line arguments: {unknown}")
161
+
162
+ # Handle version flag
163
+ if self.args.version:
164
+ try:
165
+ app_version = get_version("fast-agent-mcp")
166
+ except: # noqa: E722
167
+ app_version = "unknown"
168
+ print(f"fast-agent-mcp v{app_version}")
169
+ sys.exit(0)
170
+ # --- End of wrapped logic ---
147
171
 
148
172
  self.name = name
149
173
  self.config_path = config_path
@@ -213,137 +237,175 @@ class FastAgent:
213
237
  had_error = False
214
238
  await self.app.initialize()
215
239
 
216
- # Handle quiet mode
217
- quiet_mode = hasattr(self, "args") and self.args.quiet
218
-
219
- try:
220
- async with self.app.run():
221
- # Apply quiet mode if requested
222
- if (
223
- quiet_mode
224
- and hasattr(self.app.context, "config")
225
- and hasattr(self.app.context.config, "logger")
226
- ):
227
- # Update our app's config directly
228
- self.app.context.config.logger.progress_display = False
229
- self.app.context.config.logger.show_chat = False
230
- self.app.context.config.logger.show_tools = False
231
-
232
- # Directly disable the progress display singleton
233
- from mcp_agent.progress_display import progress_display
234
-
235
- progress_display.stop()
236
-
237
- # Pre-flight validation
238
- if 0 == len(self.agents):
239
- raise AgentConfigError("No agents defined. Please define at least one agent.")
240
- validate_server_references(self.context, self.agents)
241
- validate_workflow_references(self.agents)
242
-
243
- # Get a model factory function
244
- def model_factory_func(model=None, request_params=None):
245
- return get_model_factory(
246
- self.context,
247
- model=model,
248
- request_params=request_params,
249
- cli_model=self.args.model if hasattr(self, "args") else None,
250
- )
251
-
252
- # Create all agents in dependency order
253
- active_agents = await create_agents_in_dependency_order(
254
- self.app,
255
- self.agents,
256
- model_factory_func,
257
- )
258
-
259
- # Create a wrapper with all agents for simplified access
260
- wrapper = AgentApp(active_agents)
261
-
262
- # Handle command line options that should be processed after agent initialization
263
-
264
- # Handle --server option
265
- if hasattr(self, "args") and self.args.server:
266
- try:
267
- # Print info message if not in quiet mode
268
- if not quiet_mode:
269
- print(f"Starting FastAgent '{self.name}' in server mode")
270
- print(f"Transport: {self.args.transport}")
271
- if self.args.transport == "sse":
272
- print(f"Listening on {self.args.host}:{self.args.port}")
273
- print("Press Ctrl+C to stop")
274
-
275
- # Create the MCP server
276
- from mcp_agent.mcp_server import AgentMCPServer
277
-
278
- mcp_server = AgentMCPServer(
279
- agent_app=wrapper,
280
- server_name=f"{self.name}-MCP-Server",
281
- )
282
-
283
- # Run the server directly (this is a blocking call)
284
- await mcp_server.run_async(
285
- transport=self.args.transport, host=self.args.host, port=self.args.port
240
+ # Handle quiet mode and CLI model override safely
241
+ # Define these *before* they are used, checking if self.args exists and has the attributes
242
+ quiet_mode = hasattr(self.args, "quiet") and self.args.quiet
243
+ cli_model_override = (
244
+ self.args.model if hasattr(self.args, "model") and self.args.model else None
245
+ ) # Define cli_model_override here
246
+ tracer = trace.get_tracer(__name__)
247
+ with tracer.start_as_current_span(self.name):
248
+ try:
249
+ async with self.app.run():
250
+ # Apply quiet mode if requested
251
+ if (
252
+ quiet_mode
253
+ and hasattr(self.app.context, "config")
254
+ and hasattr(self.app.context.config, "logger")
255
+ ):
256
+ # Update our app's config directly
257
+ self.app.context.config.logger.progress_display = False
258
+ self.app.context.config.logger.show_chat = False
259
+ self.app.context.config.logger.show_tools = False
260
+
261
+ # Directly disable the progress display singleton
262
+ from mcp_agent.progress_display import progress_display
263
+
264
+ progress_display.stop()
265
+
266
+ # Pre-flight validation
267
+ if 0 == len(self.agents):
268
+ raise AgentConfigError(
269
+ "No agents defined. Please define at least one agent."
286
270
  )
287
- except KeyboardInterrupt:
288
- if not quiet_mode:
289
- print("\nServer stopped by user (Ctrl+C)")
290
- except Exception as e:
291
- if not quiet_mode:
292
- print(f"\nServer stopped with error: {e}")
293
-
294
- # Exit after server shutdown
295
- raise SystemExit(0)
296
-
297
- # Handle direct message sending if --agent and --message are provided
298
- if hasattr(self, "args") and self.args.agent and self.args.message:
299
- agent_name = self.args.agent
300
- message = self.args.message
301
-
302
- if agent_name not in active_agents:
303
- available_agents = ", ".join(active_agents.keys())
304
- print(
305
- f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}"
271
+ validate_server_references(self.context, self.agents)
272
+ validate_workflow_references(self.agents)
273
+
274
+ # Get a model factory function
275
+ # Now cli_model_override is guaranteed to be defined
276
+ def model_factory_func(model=None, request_params=None):
277
+ return get_model_factory(
278
+ self.context,
279
+ model=model,
280
+ request_params=request_params,
281
+ cli_model=cli_model_override, # Use the variable defined above
306
282
  )
307
- raise SystemExit(1)
308
-
309
- try:
310
- # Get response from the agent
311
- agent = active_agents[agent_name]
312
- response = await agent.send(message)
313
283
 
314
- # In quiet mode, just print the raw response
315
- # The chat display should already be turned off by the configuration
316
- if self.args.quiet:
317
- print(f"{response}")
284
+ # Create all agents in dependency order
285
+ active_agents = await create_agents_in_dependency_order(
286
+ self.app,
287
+ self.agents,
288
+ model_factory_func,
289
+ )
318
290
 
291
+ # Create a wrapper with all agents for simplified access
292
+ wrapper = AgentApp(active_agents)
293
+
294
+ # Handle command line options that should be processed after agent initialization
295
+
296
+ # Handle --server option
297
+ # Check if parse_cli_args was True before checking self.args.server
298
+ if hasattr(self.args, "server") and self.args.server:
299
+ try:
300
+ # Print info message if not in quiet mode
301
+ if not quiet_mode:
302
+ print(f"Starting FastAgent '{self.name}' in server mode")
303
+ print(f"Transport: {self.args.transport}")
304
+ if self.args.transport == "sse":
305
+ print(f"Listening on {self.args.host}:{self.args.port}")
306
+ print("Press Ctrl+C to stop")
307
+
308
+ # Create the MCP server
309
+ from mcp_agent.mcp_server import AgentMCPServer
310
+
311
+ mcp_server = AgentMCPServer(
312
+ agent_app=wrapper,
313
+ server_name=f"{self.name}-MCP-Server",
314
+ )
315
+
316
+ # Run the server directly (this is a blocking call)
317
+ await mcp_server.run_async(
318
+ transport=self.args.transport,
319
+ host=self.args.host,
320
+ port=self.args.port,
321
+ )
322
+ except KeyboardInterrupt:
323
+ if not quiet_mode:
324
+ print("\nServer stopped by user (Ctrl+C)")
325
+ except Exception as e:
326
+ if not quiet_mode:
327
+ print(f"\nServer stopped with error: {e}")
328
+
329
+ # Exit after server shutdown
319
330
  raise SystemExit(0)
320
- except Exception as e:
321
- print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
322
- raise SystemExit(1)
323
-
324
- yield wrapper
325
-
326
- except (
327
- ServerConfigError,
328
- ProviderKeyError,
329
- AgentConfigError,
330
- ServerInitializationError,
331
- ModelConfigError,
332
- CircularDependencyError,
333
- PromptExitError,
334
- ) as e:
335
- had_error = True
336
- self._handle_error(e)
337
- raise SystemExit(1)
338
331
 
339
- finally:
340
- # Clean up any active agents
341
- if active_agents and not had_error:
342
- for agent in active_agents.values():
343
- try:
344
- await agent.shutdown()
345
- except Exception:
346
- pass
332
+ # Handle direct message sending if --message is provided
333
+ if self.args.message:
334
+ agent_name = self.args.agent
335
+ message = self.args.message
336
+
337
+ if agent_name not in active_agents:
338
+ available_agents = ", ".join(active_agents.keys())
339
+ print(
340
+ f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}"
341
+ )
342
+ raise SystemExit(1)
343
+
344
+ try:
345
+ # Get response from the agent
346
+ agent = active_agents[agent_name]
347
+ response = await agent.send(message)
348
+
349
+ # In quiet mode, just print the raw response
350
+ # The chat display should already be turned off by the configuration
351
+ if self.args.quiet:
352
+ print(f"{response}")
353
+
354
+ raise SystemExit(0)
355
+ except Exception as e:
356
+ print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
357
+ raise SystemExit(1)
358
+
359
+ if self.args.prompt_file:
360
+ agent_name = self.args.agent
361
+ prompt: List[PromptMessageMultipart] = load_prompt_multipart(
362
+ Path(self.args.prompt_file)
363
+ )
364
+ if agent_name not in active_agents:
365
+ available_agents = ", ".join(active_agents.keys())
366
+ print(
367
+ f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}"
368
+ )
369
+ raise SystemExit(1)
370
+
371
+ try:
372
+ # Get response from the agent
373
+ agent = active_agents[agent_name]
374
+ response = await agent.generate(prompt)
375
+
376
+ # In quiet mode, just print the raw response
377
+ # The chat display should already be turned off by the configuration
378
+ if self.args.quiet:
379
+ print(f"{response.last_text()}")
380
+
381
+ raise SystemExit(0)
382
+ except Exception as e:
383
+ print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
384
+ raise SystemExit(1)
385
+
386
+ yield wrapper
387
+
388
+ except (
389
+ ServerConfigError,
390
+ ProviderKeyError,
391
+ AgentConfigError,
392
+ ServerInitializationError,
393
+ ModelConfigError,
394
+ CircularDependencyError,
395
+ PromptExitError,
396
+ ) as e:
397
+ had_error = True
398
+ self._handle_error(e)
399
+ raise SystemExit(1)
400
+
401
+ finally:
402
+ # Clean up any active agents
403
+ if active_agents and not had_error:
404
+ for agent in active_agents.values():
405
+ try:
406
+ await agent.shutdown()
407
+ except Exception:
408
+ pass
347
409
 
348
410
  def _handle_error(self, e: Exception, error_type: Optional[str] = None) -> None:
349
411
  """
@@ -153,8 +153,12 @@ class InteractivePrompt:
153
153
  )
154
154
  continue
155
155
 
156
- # Skip further processing if command was handled
157
- if command_result:
156
+ # Skip further processing if:
157
+ # 1. The command was handled (command_result is truthy)
158
+ # 2. The original input was a dictionary (special command like /prompt)
159
+ # 3. The command result itself is a dictionary (special command handling result)
160
+ # This fixes the issue where /prompt without arguments gets sent to the LLM
161
+ if command_result or isinstance(user_input, dict) or isinstance(command_result, dict):
158
162
  continue
159
163
 
160
164
  if user_input.upper() == "STOP":
@@ -231,6 +231,12 @@ def get_dependencies_groups(
231
231
  if agent_type == AgentType.PARALLEL.value:
232
232
  # Parallel agents depend on their fan-out and fan-in agents
233
233
  dependencies[name].update(agent_data.get("parallel_agents", []))
234
+ # Also add explicit fan_out dependencies if present
235
+ if "fan_out" in agent_data:
236
+ dependencies[name].update(agent_data["fan_out"])
237
+ # Add explicit fan_in dependency if present
238
+ if "fan_in" in agent_data and agent_data["fan_in"]:
239
+ dependencies[name].add(agent_data["fan_in"])
234
240
  elif agent_type == AgentType.CHAIN.value:
235
241
  # Chain agents depend on the agents in their sequence
236
242
  dependencies[name].update(agent_data.get("sequence", []))
@@ -241,7 +247,12 @@ def get_dependencies_groups(
241
247
  # Orchestrator agents depend on their child agents
242
248
  dependencies[name].update(agent_data.get("child_agents", []))
243
249
  elif agent_type == AgentType.EVALUATOR_OPTIMIZER.value:
244
- # Evaluator-Optimizer agents depend on their evaluation and optimization agents
250
+ # Evaluator-Optimizer agents depend on their evaluator and generator agents
251
+ if "evaluator" in agent_data:
252
+ dependencies[name].add(agent_data["evaluator"])
253
+ if "generator" in agent_data:
254
+ dependencies[name].add(agent_data["generator"])
255
+ # For backward compatibility - also check eval_optimizer_agents if present
245
256
  dependencies[name].update(agent_data.get("eval_optimizer_agents", []))
246
257
 
247
258
  # Check for cycles if not allowed