fast-agent-mcp 0.3.17__py3-none-any.whl → 0.3.18__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.

@@ -5,6 +5,7 @@ import logging
5
5
  import shlex
6
6
  import sys
7
7
  from pathlib import Path
8
+ from typing import Literal
8
9
 
9
10
  import typer
10
11
 
@@ -23,6 +24,50 @@ app = typer.Typer(
23
24
  default_instruction = DEFAULT_AGENT_INSTRUCTION
24
25
 
25
26
 
27
+ def resolve_instruction_option(instruction: str | None) -> tuple[str, str]:
28
+ """
29
+ Resolve the instruction option (file or URL) to the instruction string and agent name.
30
+ Returns (resolved_instruction, agent_name).
31
+ """
32
+ resolved_instruction = default_instruction
33
+ agent_name = "agent"
34
+
35
+ if instruction:
36
+ try:
37
+ from pathlib import Path
38
+
39
+ from pydantic import AnyUrl
40
+
41
+ from fast_agent.core.direct_decorators import _resolve_instruction
42
+
43
+ if instruction.startswith(("http://", "https://")):
44
+ resolved_instruction = _resolve_instruction(AnyUrl(instruction))
45
+ else:
46
+ resolved_instruction = _resolve_instruction(Path(instruction))
47
+ instruction_path = Path(instruction)
48
+ if instruction_path.exists() and instruction_path.is_file():
49
+ agent_name = instruction_path.stem
50
+ except Exception as e:
51
+ typer.echo(f"Error loading instruction from {instruction}: {e}", err=True)
52
+ raise typer.Exit(1)
53
+
54
+ return resolved_instruction, agent_name
55
+
56
+
57
+ def collect_stdio_commands(npx: str | None, uvx: str | None, stdio: str | None) -> list[str]:
58
+ """Collect STDIO command definitions from convenience options."""
59
+ stdio_commands: list[str] = []
60
+
61
+ if npx:
62
+ stdio_commands.append(f"npx {npx}")
63
+ if uvx:
64
+ stdio_commands.append(f"uvx {uvx}")
65
+ if stdio:
66
+ stdio_commands.append(stdio)
67
+
68
+ return stdio_commands
69
+
70
+
26
71
  def _set_asyncio_exception_handler(loop: asyncio.AbstractEventLoop) -> None:
27
72
  """Attach a detailed exception handler to the provided event loop."""
28
73
 
@@ -72,6 +117,12 @@ async def _run_agent(
72
117
  agent_name: str | None = "agent",
73
118
  skills_directory: Path | None = None,
74
119
  shell_runtime: bool = False,
120
+ mode: Literal["interactive", "serve"] = "interactive",
121
+ transport: str = "http",
122
+ host: str = "0.0.0.0",
123
+ port: int = 8000,
124
+ tool_description: str | None = None,
125
+ instance_scope: str = "shared",
75
126
  ) -> None:
76
127
  """Async implementation to run an interactive agent."""
77
128
  from fast_agent.mcp.prompts.prompt_load import load_prompt
@@ -84,6 +135,8 @@ async def _run_agent(
84
135
  "ignore_unknown_args": True,
85
136
  "parse_cli_args": False, # Don't parse CLI args, we're handling it ourselves
86
137
  }
138
+ if mode == "serve":
139
+ fast_kwargs["quiet"] = True
87
140
  if skills_directory is not None:
88
141
  fast_kwargs["skills_directory"] = skills_directory
89
142
 
@@ -183,7 +236,16 @@ async def _run_agent(
183
236
  await agent.interactive()
184
237
 
185
238
  # Run the agent
186
- await cli_agent()
239
+ if mode == "serve":
240
+ await fast.start_server(
241
+ transport=transport,
242
+ host=host,
243
+ port=port,
244
+ tool_description=tool_description,
245
+ instance_scope=instance_scope,
246
+ )
247
+ else:
248
+ await cli_agent()
187
249
 
188
250
 
189
251
  def run_async_agent(
@@ -200,6 +262,12 @@ def run_async_agent(
200
262
  agent_name: str | None = None,
201
263
  skills_directory: Path | None = None,
202
264
  shell_enabled: bool = False,
265
+ mode: Literal["interactive", "serve"] = "interactive",
266
+ transport: str = "http",
267
+ host: str = "0.0.0.0",
268
+ port: int = 8000,
269
+ tool_description: str | None = None,
270
+ instance_scope: str = "shared",
203
271
  ):
204
272
  """Run the async agent function with proper loop handling."""
205
273
  server_list = servers.split(",") if servers else None
@@ -304,6 +372,12 @@ def run_async_agent(
304
372
  agent_name=agent_name,
305
373
  skills_directory=skills_directory,
306
374
  shell_runtime=shell_enabled,
375
+ mode=mode,
376
+ transport=transport,
377
+ host=host,
378
+ port=port,
379
+ tool_description=tool_description,
380
+ instance_scope=instance_scope,
307
381
  )
308
382
  )
309
383
  finally:
@@ -405,46 +479,13 @@ def go(
405
479
  --stdio Command to run as STDIO MCP server (quoted)
406
480
  """
407
481
  # Collect all stdio commands from convenience options
408
- stdio_commands = []
482
+ stdio_commands = collect_stdio_commands(npx, uvx, stdio)
409
483
  shell_enabled = shell
410
484
 
411
- if npx:
412
- stdio_commands.append(f"npx {npx}")
413
-
414
- if uvx:
415
- stdio_commands.append(f"uvx {uvx}")
416
-
417
- if stdio:
418
- stdio_commands.append(stdio)
419
-
420
485
  # When shell is enabled we don't add an MCP stdio server; handled inside the agent
421
486
 
422
487
  # Resolve instruction from file/URL or use default
423
- resolved_instruction = default_instruction # Default
424
- agent_name = "agent"
425
-
426
- if instruction:
427
- try:
428
- from pathlib import Path
429
-
430
- from pydantic import AnyUrl
431
-
432
- from fast_agent.core.direct_decorators import _resolve_instruction
433
-
434
- # Check if it's a URL
435
- if instruction.startswith(("http://", "https://")):
436
- resolved_instruction = _resolve_instruction(AnyUrl(instruction))
437
- else:
438
- # Treat as file path
439
- resolved_instruction = _resolve_instruction(Path(instruction))
440
- # Extract filename without extension to use as agent name
441
- instruction_path = Path(instruction)
442
- if instruction_path.exists() and instruction_path.is_file():
443
- # Get filename without extension
444
- agent_name = instruction_path.stem
445
- except Exception as e:
446
- typer.echo(f"Error loading instruction from {instruction}: {e}", err=True)
447
- raise typer.Exit(1)
488
+ resolved_instruction, agent_name = resolve_instruction_option(instruction)
448
489
 
449
490
  run_async_agent(
450
491
  name=name,
@@ -460,4 +501,5 @@ def go(
460
501
  agent_name=agent_name,
461
502
  skills_directory=skills_dir,
462
503
  shell_enabled=shell_enabled,
504
+ instance_scope="shared",
463
505
  )
@@ -0,0 +1,136 @@
1
+ """Run FastAgent as an MCP server from the command line."""
2
+
3
+ from enum import Enum
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from fast_agent.cli.commands.go import (
9
+ collect_stdio_commands,
10
+ resolve_instruction_option,
11
+ run_async_agent,
12
+ )
13
+
14
+
15
+ class ServeTransport(str, Enum):
16
+ HTTP = "http"
17
+ SSE = "sse"
18
+ STDIO = "stdio"
19
+
20
+
21
+ class InstanceScope(str, Enum):
22
+ SHARED = "shared"
23
+ CONNECTION = "connection"
24
+ REQUEST = "request"
25
+
26
+
27
+ app = typer.Typer(
28
+ help="Run FastAgent as an MCP server without writing an agent.py file",
29
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
30
+ )
31
+
32
+
33
+ @app.callback(invoke_without_command=True, no_args_is_help=False)
34
+ def serve(
35
+ ctx: typer.Context,
36
+ name: str = typer.Option("fast-agent", "--name", help="Name for the MCP server"),
37
+ instruction: str | None = typer.Option(
38
+ None, "--instruction", "-i", help="Path to file or URL containing instruction for the agent"
39
+ ),
40
+ config_path: str | None = typer.Option(None, "--config-path", "-c", help="Path to config file"),
41
+ servers: str | None = typer.Option(
42
+ None, "--servers", help="Comma-separated list of server names to enable from config"
43
+ ),
44
+ urls: str | None = typer.Option(
45
+ None, "--url", help="Comma-separated list of HTTP/SSE URLs to connect to"
46
+ ),
47
+ auth: str | None = typer.Option(
48
+ None, "--auth", help="Bearer token for authorization with URL-based servers"
49
+ ),
50
+ model: str | None = typer.Option(
51
+ None, "--model", "--models", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
52
+ ),
53
+ skills_dir: Path | None = typer.Option(
54
+ None,
55
+ "--skills-dir",
56
+ "--skills",
57
+ help="Override the default skills directory",
58
+ ),
59
+ npx: str | None = typer.Option(
60
+ None, "--npx", help="NPX package and args to run as MCP server (quoted)"
61
+ ),
62
+ uvx: str | None = typer.Option(
63
+ None, "--uvx", help="UVX package and args to run as MCP server (quoted)"
64
+ ),
65
+ stdio: str | None = typer.Option(
66
+ None, "--stdio", help="Command to run as STDIO MCP server (quoted)"
67
+ ),
68
+ description: str | None = typer.Option(
69
+ None,
70
+ "--description",
71
+ "-d",
72
+ help="Description used for the exposed send tool (use {agent} to reference the agent name)",
73
+ ),
74
+ transport: ServeTransport = typer.Option(
75
+ ServeTransport.HTTP,
76
+ "--transport",
77
+ help="Transport protocol to expose (http, sse, stdio)",
78
+ ),
79
+ host: str = typer.Option(
80
+ "0.0.0.0",
81
+ "--host",
82
+ help="Host address to bind when using HTTP or SSE transport",
83
+ ),
84
+ port: int = typer.Option(
85
+ 8000,
86
+ "--port",
87
+ help="Port to use when running as a server with HTTP or SSE transport",
88
+ ),
89
+ shell: bool = typer.Option(
90
+ False,
91
+ "--shell",
92
+ "-x",
93
+ help="Enable a local shell runtime and expose the execute tool (bash or pwsh).",
94
+ ),
95
+ instance_scope: InstanceScope = typer.Option(
96
+ InstanceScope.SHARED,
97
+ "--instance-scope",
98
+ help="Control how MCP clients receive isolated agent instances (shared, connection, request)",
99
+ ),
100
+ ) -> None:
101
+ """
102
+ Run FastAgent as an MCP server.
103
+
104
+ Examples:
105
+ fast-agent serve --model=haiku --instruction=./instruction.md --transport=http --port=8000
106
+ fast-agent serve --url=http://localhost:8001/mcp --auth=YOUR_API_TOKEN
107
+ fast-agent serve --stdio "python my_server.py --debug"
108
+ fast-agent serve --npx "@modelcontextprotocol/server-filesystem /path/to/data"
109
+ fast-agent serve --description "Interact with the {agent} assistant"
110
+ """
111
+ stdio_commands = collect_stdio_commands(npx, uvx, stdio)
112
+ shell_enabled = shell
113
+
114
+ resolved_instruction, agent_name = resolve_instruction_option(instruction)
115
+
116
+ run_async_agent(
117
+ name=name,
118
+ instruction=resolved_instruction,
119
+ config_path=config_path,
120
+ servers=servers,
121
+ urls=urls,
122
+ auth=auth,
123
+ model=model,
124
+ message=None,
125
+ prompt_file=None,
126
+ stdio_commands=stdio_commands,
127
+ agent_name=agent_name,
128
+ skills_directory=skills_dir,
129
+ shell_enabled=shell_enabled,
130
+ mode="serve",
131
+ transport=transport.value,
132
+ host=host,
133
+ port=port,
134
+ tool_description=description,
135
+ instance_scope=instance_scope.value,
136
+ )
@@ -28,6 +28,7 @@ GO_SPECIFIC_OPTIONS = {
28
28
  # Known subcommands that should not trigger auto-routing
29
29
  KNOWN_SUBCOMMANDS = {
30
30
  "go",
31
+ "serve",
31
32
  "setup",
32
33
  "check",
33
34
  "auth",
fast_agent/cli/main.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import typer
4
4
  from rich.table import Table
5
5
 
6
- from fast_agent.cli.commands import auth, check_config, go, quickstart, setup
6
+ from fast_agent.cli.commands import auth, check_config, go, quickstart, serve, setup
7
7
  from fast_agent.cli.terminal import Application
8
8
  from fast_agent.ui.console import console as shared_console
9
9
 
@@ -14,6 +14,7 @@ app = typer.Typer(
14
14
 
15
15
  # Subcommands
16
16
  app.add_typer(go.app, name="go", help="Run an interactive agent directly from the command line")
17
+ app.add_typer(serve.app, name="serve", help="Run FastAgent as an MCP server")
17
18
  app.add_typer(setup.app, name="setup", help="Set up a new agent project")
18
19
  app.add_typer(check_config.app, name="check", help="Show or diagnose fast-agent configuration")
19
20
  app.add_typer(auth.app, name="auth", help="Manage OAuth authentication for MCP servers")
@@ -62,7 +63,8 @@ def show_welcome() -> None:
62
63
  table.add_column("Description", header_style="bold bright_white")
63
64
 
64
65
  table.add_row("[bold]go[/bold]", "Start an interactive session")
65
- table.add_row("go --shell", "Start an interactive session with a local shell tool")
66
+ table.add_row("go -x", "Start an interactive session with a local shell tool")
67
+ table.add_row("[bold]serve[/bold]", "Start fast-agent as an MCP server")
66
68
  table.add_row("check", "Show current configuration")
67
69
  table.add_row("auth", "Manage OAuth tokens and keyring")
68
70
  table.add_row("setup", "Create agent template and configuration")
@@ -6,9 +6,11 @@ directly creates Agent instances without proxies.
6
6
 
7
7
  import argparse
8
8
  import asyncio
9
+ import inspect
9
10
  import pathlib
10
11
  import sys
11
12
  from contextlib import asynccontextmanager
13
+ from dataclasses import dataclass
12
14
  from importlib.metadata import version as get_version
13
15
  from pathlib import Path
14
16
  from typing import (
@@ -127,6 +129,9 @@ class FastAgent:
127
129
  Path(skills_directory).expanduser() if skills_directory else None
128
130
  )
129
131
  self._default_skill_manifests: List[SkillManifest] = []
132
+ self._server_instance_factory = None
133
+ self._server_instance_dispose = None
134
+ self._server_managed_instances: List[AgentInstance] = []
130
135
 
131
136
  # --- Wrap argument parsing logic ---
132
137
  if parse_cli_args:
@@ -181,6 +186,12 @@ class FastAgent:
181
186
  default="0.0.0.0",
182
187
  help="Host address to bind to when running as a server with SSE transport",
183
188
  )
189
+ parser.add_argument(
190
+ "--instance-scope",
191
+ choices=["shared", "connection", "request"],
192
+ default="shared",
193
+ help="Control MCP agent instancing behaviour (shared, connection, request)",
194
+ )
184
195
  parser.add_argument(
185
196
  "--skills",
186
197
  help="Path to skills directory to use instead of default .claude/skills",
@@ -500,18 +511,34 @@ class FastAgent:
500
511
  cli_model=cli_model_override, # Use the variable defined above
501
512
  )
502
513
 
503
- # Create all agents in dependency order
504
- active_agents = await create_agents_in_dependency_order(
505
- self.app,
506
- self.agents,
507
- model_factory_func,
508
- )
514
+ managed_instances: list[AgentInstance] = []
515
+ instance_lock = asyncio.Lock()
516
+
517
+ async def instantiate_agent_instance() -> AgentInstance:
518
+ async with instance_lock:
519
+ agents_map = await create_agents_in_dependency_order(
520
+ self.app,
521
+ self.agents,
522
+ model_factory_func,
523
+ )
524
+ validate_provider_keys_post_creation(agents_map)
525
+ instance = AgentInstance(AgentApp(agents_map), agents_map)
526
+ managed_instances.append(instance)
527
+ return instance
528
+
529
+ async def dispose_agent_instance(instance: AgentInstance) -> None:
530
+ async with instance_lock:
531
+ if instance in managed_instances:
532
+ managed_instances.remove(instance)
533
+ await instance.shutdown()
509
534
 
510
- # Validate API keys after agent creation
511
- validate_provider_keys_post_creation(active_agents)
535
+ primary_instance = await instantiate_agent_instance()
536
+ wrapper = primary_instance.app
537
+ active_agents = primary_instance.agents
512
538
 
513
- # Create a wrapper with all agents for simplified access
514
- wrapper = AgentApp(active_agents)
539
+ self._server_instance_factory = instantiate_agent_instance
540
+ self._server_instance_dispose = dispose_agent_instance
541
+ self._server_managed_instances = managed_instances
515
542
 
516
543
  # Disable streaming if parallel agents are present
517
544
  from fast_agent.agents.agent_types import AgentType
@@ -541,9 +568,18 @@ class FastAgent:
541
568
  # Create the MCP server
542
569
  from fast_agent.mcp.server import AgentMCPServer
543
570
 
571
+ tool_description = getattr(self.args, "tool_description", None)
572
+ server_description = getattr(self.args, "server_description", None)
573
+ server_name = getattr(self.args, "server_name", None)
574
+ instance_scope = getattr(self.args, "instance_scope", "shared")
544
575
  mcp_server = AgentMCPServer(
545
- agent_app=wrapper,
546
- server_name=f"{self.name}-MCP-Server",
576
+ primary_instance=primary_instance,
577
+ create_instance=self._server_instance_factory,
578
+ dispose_instance=self._server_instance_dispose,
579
+ instance_scope=instance_scope,
580
+ server_name=server_name or f"{self.name}-MCP-Server",
581
+ server_description=server_description,
582
+ tool_description=tool_description,
547
583
  )
548
584
 
549
585
  # Run the server directly (this is a blocking call)
@@ -647,11 +683,32 @@ class FastAgent:
647
683
  pass
648
684
 
649
685
  # Print usage report before cleanup (show for user exits too)
650
- if active_agents and not had_error and not quiet_mode:
686
+ if (
687
+ getattr(self, "_server_managed_instances", None)
688
+ and not had_error
689
+ and not quiet_mode
690
+ and getattr(self.args, "server", False) is False
691
+ ):
692
+ # Only show usage report for non-server interactive runs
693
+ if managed_instances:
694
+ instance = managed_instances[0]
695
+ self._print_usage_report(instance.agents)
696
+ elif active_agents and not had_error and not quiet_mode:
651
697
  self._print_usage_report(active_agents)
652
698
 
653
699
  # Clean up any active agents (always cleanup, even on errors)
654
- if active_agents:
700
+ if getattr(self, "_server_managed_instances", None) and getattr(
701
+ self, "_server_instance_dispose", None
702
+ ):
703
+ # Dispose any remaining instances
704
+ remaining_instances = list(self._server_managed_instances)
705
+ for instance in remaining_instances:
706
+ try:
707
+ await self._server_instance_dispose(instance)
708
+ except Exception:
709
+ pass
710
+ self._server_managed_instances.clear()
711
+ elif active_agents:
655
712
  for agent in active_agents.values():
656
713
  try:
657
714
  await agent.shutdown()
@@ -790,6 +847,8 @@ class FastAgent:
790
847
  port: int = 8000,
791
848
  server_name: Optional[str] = None,
792
849
  server_description: Optional[str] = None,
850
+ tool_description: Optional[str] = None,
851
+ instance_scope: str = "shared",
793
852
  ) -> None:
794
853
  """
795
854
  Start the application as an MCP server.
@@ -801,7 +860,9 @@ class FastAgent:
801
860
  host: Host address for the server when using SSE
802
861
  port: Port for the server when using SSE
803
862
  server_name: Optional custom name for the MCP server
804
- server_description: Optional description for the MCP server
863
+ server_description: Optional description/instructions for the MCP server
864
+ tool_description: Optional description template for the exposed send tool.
865
+ Use {agent} to reference the agent name.
805
866
  """
806
867
  # This method simply updates the command line arguments and uses run()
807
868
  # to ensure we follow the same initialization path for all operations
@@ -819,6 +880,10 @@ class FastAgent:
819
880
  self.args.transport = transport
820
881
  self.args.host = host
821
882
  self.args.port = port
883
+ self.args.tool_description = tool_description
884
+ self.args.server_description = server_description
885
+ self.args.server_name = server_name
886
+ self.args.instance_scope = instance_scope
822
887
  self.args.quiet = (
823
888
  original_args.quiet if original_args and hasattr(original_args, "quiet") else False
824
889
  )
@@ -842,6 +907,8 @@ class FastAgent:
842
907
  port: int = 8000,
843
908
  server_name: Optional[str] = None,
844
909
  server_description: Optional[str] = None,
910
+ tool_description: Optional[str] = None,
911
+ instance_scope: str = "shared",
845
912
  ) -> None:
846
913
  """
847
914
  Run the application and expose agents through an MCP server.
@@ -853,7 +920,8 @@ class FastAgent:
853
920
  host: Host address for the server when using SSE
854
921
  port: Port for the server when using SSE
855
922
  server_name: Optional custom name for the MCP server
856
- server_description: Optional description for the MCP server
923
+ server_description: Optional description/instructions for the MCP server
924
+ tool_description: Optional description template for the exposed send tool.
857
925
  """
858
926
  await self.start_server(
859
927
  transport=transport,
@@ -861,6 +929,8 @@ class FastAgent:
861
929
  port=port,
862
930
  server_name=server_name,
863
931
  server_description=server_description,
932
+ tool_description=tool_description,
933
+ instance_scope=instance_scope,
864
934
  )
865
935
 
866
936
  async def main(self):
@@ -892,3 +962,19 @@ class FastAgent:
892
962
  # Just check if the flag is set, no action here
893
963
  # The actual server code will be handled by run()
894
964
  return hasattr(self, "args") and self.args.server
965
+ @dataclass
966
+ class AgentInstance:
967
+ app: AgentApp
968
+ agents: Dict[str, "AgentProtocol"]
969
+
970
+ async def shutdown(self) -> None:
971
+ for agent in self.agents.values():
972
+ try:
973
+ shutdown = getattr(agent, "shutdown", None)
974
+ if shutdown is None:
975
+ continue
976
+ result = shutdown()
977
+ if inspect.isawaitable(result):
978
+ await result
979
+ except Exception:
980
+ pass
@@ -3,17 +3,19 @@ Enhanced AgentMCPServer with robust shutdown handling for SSE transport.
3
3
  """
4
4
 
5
5
  import asyncio
6
+ import logging
6
7
  import os
7
8
  import signal
9
+ import time
8
10
  from contextlib import AsyncExitStack, asynccontextmanager
9
- from typing import Set
11
+ from typing import Awaitable, Callable, Set
10
12
 
11
13
  from mcp.server.fastmcp import Context as MCPContext
12
14
  from mcp.server.fastmcp import FastMCP
13
15
 
14
16
  import fast_agent.core
15
17
  import fast_agent.core.prompt
16
- from fast_agent.core.agent_app import AgentApp
18
+ from fast_agent.core.fastagent import AgentInstance
17
19
  from fast_agent.core.logging.logger import get_logger
18
20
 
19
21
  logger = get_logger(__name__)
@@ -24,17 +26,29 @@ class AgentMCPServer:
24
26
 
25
27
  def __init__(
26
28
  self,
27
- agent_app: AgentApp,
29
+ primary_instance: AgentInstance,
30
+ create_instance: Callable[[], Awaitable[AgentInstance]],
31
+ dispose_instance: Callable[[AgentInstance], Awaitable[None]],
32
+ instance_scope: str,
28
33
  server_name: str = "FastAgent-MCP-Server",
29
34
  server_description: str | None = None,
35
+ tool_description: str | None = None,
30
36
  ) -> None:
31
37
  """Initialize the server with the provided agent app."""
32
- self.agent_app = agent_app
38
+ self.primary_instance = primary_instance
39
+ self._create_instance_task = create_instance
40
+ self._dispose_instance_task = dispose_instance
41
+ self._instance_scope = instance_scope
33
42
  self.mcp_server: FastMCP = FastMCP(
34
43
  name=server_name,
35
44
  instructions=server_description
36
- or f"This server provides access to {len(agent_app._agents)} agents",
45
+ or f"This server provides access to {len(primary_instance.agents)} agents",
37
46
  )
47
+ if self._instance_scope == "request":
48
+ # Ensure FastMCP does not attempt to maintain sessions for stateless mode
49
+ self.mcp_server.settings.stateless_http = True
50
+ self._tool_description = tool_description
51
+ self._shared_instance_active = True
38
52
  # Shutdown coordination
39
53
  self._graceful_shutdown_event = asyncio.Event()
40
54
  self._force_shutdown_event = asyncio.Event()
@@ -47,59 +61,187 @@ class AgentMCPServer:
47
61
  # Server state
48
62
  self._server_task = None
49
63
 
64
+ # Standard logging channel so we appear alongside Uvicorn/logging output
65
+ self.std_logger = logging.getLogger("fast_agent.server")
66
+
67
+ # Connection-scoped instance tracking
68
+ self._connection_instances: dict[int, AgentInstance] = {}
69
+ self._connection_cleanup_tasks: dict[int, Callable[[], Awaitable[None]]] = {}
70
+ self._connection_lock = asyncio.Lock()
71
+
50
72
  # Set up agent tools
51
73
  self.setup_tools()
52
74
 
53
- logger.info(f"AgentMCPServer initialized with {len(agent_app._agents)} agents")
75
+ logger.info(
76
+ f"AgentMCPServer initialized with {len(primary_instance.agents)} agents",
77
+ name="mcp_server_initialized",
78
+ agent_count=len(primary_instance.agents),
79
+ instance_scope=instance_scope,
80
+ )
54
81
 
55
82
  def setup_tools(self) -> None:
56
83
  """Register all agents as MCP tools."""
57
- for agent_name, agent in self.agent_app._agents.items():
58
- self.register_agent_tools(agent_name, agent)
84
+ for agent_name in self.primary_instance.agents.keys():
85
+ self.register_agent_tools(agent_name)
59
86
 
60
- def register_agent_tools(self, agent_name: str, agent) -> None:
87
+ def register_agent_tools(self, agent_name: str) -> None:
61
88
  """Register tools for a specific agent."""
62
89
 
63
90
  # Basic send message tool
91
+ tool_description = (
92
+ self._tool_description.format(agent=agent_name)
93
+ if self._tool_description and "{agent}" in self._tool_description
94
+ else self._tool_description
95
+ )
96
+
64
97
  @self.mcp_server.tool(
65
98
  name=f"{agent_name}_send",
66
- description=f"Send a message to the {agent_name} agent",
99
+ description=tool_description or f"Send a message to the {agent_name} agent",
67
100
  structured_output=False,
68
101
  # MCP 1.10.1 turns every tool in to a structured output
69
102
  )
70
103
  async def send_message(message: str, ctx: MCPContext) -> str:
71
104
  """Send a message to the agent and return its response."""
72
- # Get the agent's context
105
+ instance = await self._acquire_instance(ctx)
106
+ agent = instance.app[agent_name]
73
107
  agent_context = getattr(agent, "context", None)
74
108
 
75
109
  # Define the function to execute
76
110
  async def execute_send():
77
- return await agent.send(message)
111
+ start = time.perf_counter()
112
+ logger.info(
113
+ f"MCP request received for agent '{agent_name}'",
114
+ name="mcp_request_start",
115
+ agent=agent_name,
116
+ session=self._session_identifier(ctx),
117
+ )
118
+ self.std_logger.info(
119
+ "MCP request received for agent '%s' (scope=%s)",
120
+ agent_name,
121
+ self._instance_scope,
122
+ )
123
+
124
+ response = await agent.send(message)
125
+ duration = time.perf_counter() - start
126
+
127
+ logger.info(
128
+ f"Agent '{agent_name}' completed MCP request",
129
+ name="mcp_request_complete",
130
+ agent=agent_name,
131
+ duration=duration,
132
+ session=self._session_identifier(ctx),
133
+ )
134
+ self.std_logger.info(
135
+ "Agent '%s' completed MCP request in %.2fs (scope=%s)",
136
+ agent_name,
137
+ duration,
138
+ self._instance_scope,
139
+ )
140
+ return response
78
141
 
79
- # Execute with bridged context
80
- if agent_context and ctx:
81
- return await self.with_bridged_context(agent_context, ctx, execute_send)
82
- else:
142
+ try:
143
+ # Execute with bridged context
144
+ if agent_context and ctx:
145
+ return await self.with_bridged_context(agent_context, ctx, execute_send)
83
146
  return await execute_send()
147
+ finally:
148
+ await self._release_instance(ctx, instance)
84
149
 
85
150
  # Register a history prompt for this agent
86
151
  @self.mcp_server.prompt(
87
152
  name=f"{agent_name}_history",
88
153
  description=f"Conversation history for the {agent_name} agent",
89
154
  )
90
- async def get_history_prompt() -> list:
155
+ async def get_history_prompt(ctx: MCPContext) -> list:
91
156
  """Return the conversation history as MCP messages."""
92
- # Get the conversation history from the agent's LLM
93
- if not hasattr(agent, "_llm") or agent._llm is None:
94
- return []
157
+ instance = await self._acquire_instance(ctx)
158
+ agent = instance.app[agent_name]
159
+ try:
160
+ if not hasattr(agent, "_llm") or agent._llm is None:
161
+ return []
95
162
 
96
- # Convert the multipart message history to standard PromptMessages
97
- multipart_history = agent._llm.message_history
98
- prompt_messages = fast_agent.core.prompt.Prompt.from_multipart(multipart_history)
163
+ # Convert the multipart message history to standard PromptMessages
164
+ multipart_history = agent._llm.message_history
165
+ prompt_messages = fast_agent.core.prompt.Prompt.from_multipart(multipart_history)
99
166
 
100
- # In FastMCP, we need to return the raw list of messages
101
- # that matches the structure that FastMCP expects (list of dicts with role/content)
102
- return [{"role": msg.role, "content": msg.content} for msg in prompt_messages]
167
+ # In FastMCP, we need to return the raw list of messages
168
+ return [{"role": msg.role, "content": msg.content} for msg in prompt_messages]
169
+ finally:
170
+ await self._release_instance(ctx, instance, reuse_connection=True)
171
+
172
+ async def _acquire_instance(self, ctx: MCPContext | None) -> AgentInstance:
173
+ if self._instance_scope == "shared":
174
+ return self.primary_instance
175
+
176
+ if self._instance_scope == "request":
177
+ return await self._create_instance_task()
178
+
179
+ # Connection scope
180
+ assert ctx is not None, "Context is required for connection-scoped instances"
181
+ session_key = self._connection_key(ctx)
182
+ async with self._connection_lock:
183
+ instance = self._connection_instances.get(session_key)
184
+ if instance is None:
185
+ instance = await self._create_instance_task()
186
+ self._connection_instances[session_key] = instance
187
+ self._register_session_cleanup(ctx, session_key)
188
+ return instance
189
+
190
+ async def _release_instance(
191
+ self,
192
+ ctx: MCPContext | None,
193
+ instance: AgentInstance,
194
+ *,
195
+ reuse_connection: bool = False,
196
+ ) -> None:
197
+ if self._instance_scope == "request":
198
+ await self._dispose_instance_task(instance)
199
+ elif self._instance_scope == "connection" and reuse_connection is False:
200
+ # Connection-scoped instances persist until session cleanup
201
+ pass
202
+
203
+ def _connection_key(self, ctx: MCPContext) -> int:
204
+ return id(ctx.session)
205
+
206
+ def _register_session_cleanup(self, ctx: MCPContext, session_key: int) -> None:
207
+ async def cleanup() -> None:
208
+ instance = self._connection_instances.pop(session_key, None)
209
+ if instance is not None:
210
+ await self._dispose_instance_task(instance)
211
+
212
+ exit_stack = getattr(ctx.session, "_exit_stack", None)
213
+ if exit_stack is not None:
214
+ exit_stack.push_async_callback(cleanup)
215
+ else:
216
+ self._connection_cleanup_tasks[session_key] = cleanup
217
+
218
+ def _session_identifier(self, ctx: MCPContext | None) -> str | None:
219
+ if ctx is None:
220
+ return None
221
+ request = getattr(ctx.request_context, "request", None)
222
+ if request is not None:
223
+ return request.headers.get("mcp-session-id")
224
+ return None
225
+
226
+ async def _dispose_primary_instance(self) -> None:
227
+ if self._shared_instance_active:
228
+ try:
229
+ await self._dispose_instance_task(self.primary_instance)
230
+ finally:
231
+ self._shared_instance_active = False
232
+
233
+ async def _dispose_all_connection_instances(self) -> None:
234
+ pending_cleanups = list(self._connection_cleanup_tasks.values())
235
+ self._connection_cleanup_tasks.clear()
236
+ for cleanup in pending_cleanups:
237
+ await cleanup()
238
+
239
+ async with self._connection_lock:
240
+ instances = list(self._connection_instances.values())
241
+ self._connection_instances.clear()
242
+
243
+ for instance in instances:
244
+ await self._dispose_instance_task(instance)
103
245
 
104
246
  def _setup_signal_handlers(self):
105
247
  """Set up signal handlers for graceful and forced shutdown."""
@@ -414,14 +556,8 @@ class AgentMCPServer:
414
556
  """Minimal cleanup for STDIO transport to avoid keeping process alive."""
415
557
  logger.info("Performing minimal STDIO cleanup")
416
558
 
417
- # Just clean up agent resources directly without the full shutdown sequence
418
- # This preserves the natural exit process for STDIO
419
- for agent_name, agent in self.agent_app._agents.items():
420
- try:
421
- if hasattr(agent, "shutdown"):
422
- await agent.shutdown()
423
- except Exception as e:
424
- logger.error(f"Error shutting down agent {agent_name}: {e}")
559
+ await self._dispose_primary_instance()
560
+ await self._dispose_all_connection_instances()
425
561
 
426
562
  logger.info("STDIO cleanup complete")
427
563
 
@@ -443,13 +579,11 @@ class AgentMCPServer:
443
579
  # Close any resources in the exit stack
444
580
  await self._exit_stack.aclose()
445
581
 
446
- # Shutdown any agent resources
447
- for agent_name, agent in self.agent_app._agents.items():
448
- try:
449
- if hasattr(agent, "shutdown"):
450
- await agent.shutdown()
451
- except Exception as e:
452
- logger.error(f"Error shutting down agent {agent_name}: {e}")
582
+ # Dispose connection-scoped instances
583
+ await self._dispose_all_connection_instances()
584
+
585
+ # Dispose shared instance if still active
586
+ await self._dispose_primary_instance()
453
587
  except Exception as e:
454
588
  # Log any errors but don't let them prevent shutdown
455
589
  logger.error(f"Error during shutdown: {e}", exc_info=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-agent-mcp
3
- Version: 0.3.17
3
+ Version: 0.3.18
4
4
  Summary: Define, Prompt and Test MCP enabled Agents and Workflows
5
5
  Author-email: Shaun Smith <fastagent@llmindset.co.uk>
6
6
  License: Apache License
@@ -261,6 +261,14 @@ The simple declarative syntax lets you concentrate on composing your Prompts and
261
261
 
262
262
  Model support is comprehensive with native support for Anthropic, OpenAI and Google providers as well as Azure, Ollama, Deepseek and dozens of others via TensorZero. Structured Outputs, PDF and Vision support is simple to use and well tested. Passthrough and Playback LLMs enable rapid development and test of Python glue-code for your applications.
263
263
 
264
+ Recent features include:
265
+ - Agent Skills (SKILL.md)
266
+ - MCP-UI Support |
267
+ - OpenAI Apps SDK (Skybridge)
268
+ - Shell Mode
269
+ - Advanced MCP Transport Diagnsotics
270
+ - MCP Elicitations
271
+
264
272
  <img width="800" alt="MCP Transport Diagnostics" src="https://github.com/user-attachments/assets/e26472de-58d9-4726-8bdd-01eb407414cf" />
265
273
 
266
274
 
@@ -22,13 +22,14 @@ fast_agent/agents/workflow/parallel_agent.py,sha256=DlJXDURAfx-WBF297tKBLfH93gDF
22
22
  fast_agent/agents/workflow/router_agent.py,sha256=gugcp0a-3Ldn42JbPJZ7AbO2G6FvqE0A3tsWcLorwSY,11400
23
23
  fast_agent/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  fast_agent/cli/__main__.py,sha256=OWuKPFEeZRco6j5VA3IvtwTvZ351pDGxuO6rAiOTFNI,2588
25
- fast_agent/cli/constants.py,sha256=9XaMlY8hy7PuwDFmUg_22vGGVKYRfNpODL5pub1Wem4,659
26
- fast_agent/cli/main.py,sha256=8wBh605HLcxyr11T_I8c5_LjHHGB2UcfiY2-3F8kRnY,4493
25
+ fast_agent/cli/constants.py,sha256=xLxQKuQzbv3Qkd6CB5wNwyDqyHBvdx97PZfsoK7-b4o,672
26
+ fast_agent/cli/main.py,sha256=yXglgZuQrThhqK3EgVstwpU3YRUf2-RWIDXDb6rQ66s,4650
27
27
  fast_agent/cli/terminal.py,sha256=tDN1fJ91Nc_wZJTNafkQuD7Z7gFscvo1PHh-t7Wl-5s,1066
28
28
  fast_agent/cli/commands/auth.py,sha256=nJEC7zrz5UXYUz5O6AgGZnfJPHIrgHk68CUwGo-7Nyg,15063
29
29
  fast_agent/cli/commands/check_config.py,sha256=Iy5MHsRXqRcFLN-0Gs20jA3DqT-gN1VE9WtKuWlxJ9M,32597
30
- fast_agent/cli/commands/go.py,sha256=nNlFZ9dNPkm4VTkFmv1hDvWu8qbWsyTY5BEX32HJfmA,17580
30
+ fast_agent/cli/commands/go.py,sha256=f0CrNkjzuIWWewUnCGeMWcO-93PdhyOqq5n3a-8cWVM,19005
31
31
  fast_agent/cli/commands/quickstart.py,sha256=UOTqAbaVGLECHkTvpUNQ41PWXssqCijVvrqh30YUqnM,20624
32
+ fast_agent/cli/commands/serve.py,sha256=KU9QsP9MQVKV5eOrt8eBGi9gyx_Y20oaEwurDuWAc0k,4427
32
33
  fast_agent/cli/commands/server_helpers.py,sha256=Nuded8sZb4Rybwoq5LbXXUgwtJZg-OO04xhmPUp6e98,4073
33
34
  fast_agent/cli/commands/setup.py,sha256=n5hVjXkKTmuiW8-0ezItVcMHJ92W6NlE2JOGCYiKw0A,6388
34
35
  fast_agent/cli/commands/url_parser.py,sha256=v9KoprPBEEST5Fo7qXgbW50GC5vMpxFteKqAT6mFkdI,5991
@@ -39,7 +40,7 @@ fast_agent/core/direct_decorators.py,sha256=Z8zM1Ep9THEiuNzlxW-WmQDm7x4JF0wn4xO2
39
40
  fast_agent/core/direct_factory.py,sha256=SYYVtEqPQh7ElvAVC_445a5pZkKstM0RhKNGZ2CJQT8,18338
40
41
  fast_agent/core/error_handling.py,sha256=tZkO8LnXO-qf6jD8a12Pv5fD4NhnN1Ag5_tJ6DwbXjg,631
41
42
  fast_agent/core/exceptions.py,sha256=ENAD_qGG67foxy6vDkIvc-lgopIUQy6O7zvNPpPXaQg,2289
42
- fast_agent/core/fastagent.py,sha256=4rsKQVuK3GRW9ejT7oSfqsrNsfYb2rPpBT0wJxk_-mM,35372
43
+ fast_agent/core/fastagent.py,sha256=u9TBdQxBlyV3OhZ5gfOB2TucdI3xI2xsEObW-c1WTdA,39865
43
44
  fast_agent/core/prompt.py,sha256=qNUFlK3KtU7leYysYUglzBYQnEYiXu__iR_T8189zc0,203
44
45
  fast_agent/core/validation.py,sha256=cesQeT8jfLlPqew6S9bq8ZJqde7ViVQXHF9fhAnyOHI,11950
45
46
  fast_agent/core/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -135,7 +136,7 @@ fast_agent/mcp/prompts/prompt_load.py,sha256=1KixVXpkML3_kY3vC59AlMfcBXQVESrII24
135
136
  fast_agent/mcp/prompts/prompt_server.py,sha256=ZuUFqYfiYYE8yTXw0qagGWkZkelQgETGBpzUIdIhLXc,20174
136
137
  fast_agent/mcp/prompts/prompt_template.py,sha256=SAklXs9wujYqqEWv5vzRI_cdjSmGnWlDmPLZ5qkXZO4,15695
137
138
  fast_agent/mcp/server/__init__.py,sha256=AJFNzdmuHPRL3jqFhDVDJste_zYE_KJ3gGYDsbghvl0,156
138
- fast_agent/mcp/server/agent_server.py,sha256=zJMFnPfXPsbLt5ZyGMTN90YzIzwV7TaSQ4WYs9lnB2Q,20194
139
+ fast_agent/mcp/server/agent_server.py,sha256=svO0_RwJX1thRRfciLXkbLUFLokkzw5VspGIs5Ias40,25555
139
140
  fast_agent/resources/examples/data-analysis/analysis-campaign.py,sha256=SnDQm_e_cm0IZEKdWizUedXxpIWWj-5K70wmcM1Tw2Y,7277
140
141
  fast_agent/resources/examples/data-analysis/analysis.py,sha256=0W1dN3SAx-RxEKoH3shAq42HxglEz9hc8BadGYmnwAk,2653
141
142
  fast_agent/resources/examples/data-analysis/fastagent.config.yaml,sha256=ini94PHyJCfgpjcjHKMMbGuHs6LIj46F1NwY0ll5HVk,1609
@@ -218,8 +219,8 @@ fast_agent/ui/streaming.py,sha256=SNLGx6s8xuRikxi6vNxnDCgQQKqFJ5rbTKU9ixnzmY0,22
218
219
  fast_agent/ui/streaming_buffer.py,sha256=e-zwVUVBOQ_mKyHgLiTXFmShGs4DNQRZ9BZZwWgXoWM,16648
219
220
  fast_agent/ui/tool_display.py,sha256=DK6kA8MBjxq8qpK1WLv7kFKAfOtEOfY5rtQ8t4XeM0s,16111
220
221
  fast_agent/ui/usage_display.py,sha256=ltJpn_sDzo8PDNSXWx-QdEUbQWUnhmajCItNt5mA5rM,7285
221
- fast_agent_mcp-0.3.17.dist-info/METADATA,sha256=DyQQDnwC9okrklDtQ5MDxTxCrnRV_Z07SUQFJImJZ88,32083
222
- fast_agent_mcp-0.3.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
223
- fast_agent_mcp-0.3.17.dist-info/entry_points.txt,sha256=i6Ujja9J-hRxttOKqTYdbYP_tyaS4gLHg53vupoCSsg,199
224
- fast_agent_mcp-0.3.17.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
225
- fast_agent_mcp-0.3.17.dist-info/RECORD,,
222
+ fast_agent_mcp-0.3.18.dist-info/METADATA,sha256=UluPke36Pp1VhO8z8hmPIlC-MjqN94xO6N02HF3EWbU,32259
223
+ fast_agent_mcp-0.3.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
224
+ fast_agent_mcp-0.3.18.dist-info/entry_points.txt,sha256=i6Ujja9J-hRxttOKqTYdbYP_tyaS4gLHg53vupoCSsg,199
225
+ fast_agent_mcp-0.3.18.dist-info/licenses/LICENSE,sha256=Gx1L3axA4PnuK4FxsbX87jQ1opoOkSFfHHSytW6wLUU,10935
226
+ fast_agent_mcp-0.3.18.dist-info/RECORD,,