Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fast-agent-mcp"
version = "0.7.3"
version = "0.7.4"
description = "Define, Prompt and Test MCP enabled Agents and Workflows"
readme = "README.md"
license = { file = "LICENSE" }
Expand All @@ -15,14 +15,14 @@ classifiers = [
requires-python = ">=3.13.5,<3.15"
dependencies = [
"fastapi==0.136.1",
"fastmcp==3.2.4",
"fastmcp==3.3.1",
"mcp==1.27.1",
"pydantic-settings==2.13.0",
"pydantic==2.13.3",
"pydantic-settings==2.14.1",
"pydantic==2.13.4",
"pyyaml==6.0.3",
"rich==15.0.0",
"typer==0.25.1",
"anthropic[vertex]==0.100.0",
"anthropic[vertex]==0.102.0",
"openai[aiohttp]==2.36.0",
"prompt-toolkit==3.0.52",
"aiohttp==3.13.5",
Expand All @@ -32,21 +32,21 @@ dependencies = [
"opentelemetry-instrumentation-anthropic==0.52.1; python_version >= '3.10' and python_version < '4.0'",
"opentelemetry-instrumentation-mcp==0.52.1; python_version >= '3.10' and python_version < '4.0'",
"opentelemetry-instrumentation-google-genai==0.6b0",
"google-genai==2.0.0",
"google-genai==2.3.0",
"deprecated==1.3.1",
"a2a-sdk==0.3.26",
"a2a-sdk==1.0.3",
"email-validator==2.2.0",
"pyperclip==1.9.0",
"pyperclip==1.11.0",
"keyring==25.7.0",
"python-frontmatter==1.1.0",
"watchfiles==1.1.1",
"agent-client-protocol==0.9.0",
"jsonschema==4.25.1",
"tiktoken==0.12.0",
"agent-client-protocol==0.10.0",
"jsonschema==4.26.0",
"tiktoken==0.13.0",
"uvloop==0.22.1; platform_system != 'Windows'",
"multilspy==0.0.15",
"ruamel.yaml==0.19.1",
"huggingface_hub==1.14.0",
"huggingface_hub==1.15.0",
"mslex==1.3.0",
]

Expand Down
2 changes: 1 addition & 1 deletion resources/shared/smart_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Use `list_resources` to discover bundled internal resources and attached MCP res
Use the smart tool to load AgentCards temporarily when you need extra agents.
Use `create_agent_card` to scaffold a minimal card file quickly.
Use validate to check AgentCard files before running them.
Use `attach_resource` when you want to send a prompt with one resource attached.
Use `attach_media` when you want to send local or provider-fetchable media/document content with the next prompt.
Use `slash_command` when you need interactive-style `/...` command behavior (for example `/mcp ...`, `/skills ...`, `/cards ...`).
When calling child-agent tools (`agent__*`), follow each tool's schema and
parameter descriptions exactly.
Expand Down
8 changes: 8 additions & 0 deletions src/fast_agent/acp/server/agent_acp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,19 +663,23 @@ async def _initialize_session_state(

async def list_sessions(
self,
additional_directories: list[str] | None = None,
cursor: str | None = None,
cwd: str | None = None,
**kwargs: Any,
) -> ListSessionsResponse:
_ = additional_directories
return await self._session_store.list_sessions(cursor=cursor, cwd=cwd, **kwargs)

async def load_session(
self,
cwd: str,
session_id: str,
additional_directories: list[str] | None = None,
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> LoadSessionResponse | None:
_ = additional_directories
return await self._session_store.load_session(
cwd=cwd,
session_id=session_id,
Expand All @@ -687,9 +691,11 @@ async def resume_session(
self,
cwd: str,
session_id: str,
additional_directories: list[str] | None = None,
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> ResumeSessionResponse:
_ = additional_directories
return await self._session_store.resume_session(
cwd=cwd,
session_id=session_id,
Expand All @@ -700,6 +706,7 @@ async def resume_session(
async def new_session(
self,
cwd: str,
additional_directories: list[str] | None = None,
mcp_servers: list[HttpMcpServer | SseMcpServer | McpServerStdio] | None = None,
**kwargs: Any,
) -> NewSessionResponse:
Expand All @@ -708,6 +715,7 @@ async def new_session(

Creates a new ACP session with its own dedicated agent instance.
"""
_ = additional_directories
request_cwd = self._resolve_request_cwd(
cwd=cwd,
request_name="session/new",
Expand Down
4 changes: 1 addition & 3 deletions src/fast_agent/agents/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@

logger = get_logger(__name__)

DEFAULT_CAPABILITIES = AgentCapabilities(
streaming=False, push_notifications=False, state_transition_history=False
)
DEFAULT_CAPABILITIES = AgentCapabilities(streaming=False, push_notifications=False)


class LlmAgent(LlmDecorator):
Expand Down
9 changes: 7 additions & 2 deletions src/fast_agent/agents/llm_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from fast_agent.agents.tool_runner import ToolRunnerHooks
from fast_agent.hooks.lifecycle_hook_loader import AgentLifecycleHooks

from a2a.types import AgentCard
from a2a.types import AgentCard, AgentInterface
from mcp import ListToolsResult, Tool
from mcp.types import (
CallToolResult,
Expand Down Expand Up @@ -1446,7 +1446,12 @@ async def agent_card(self) -> AgentCard:
skills=[],
name=self._name,
description=self.config.description or self.instruction,
url=f"fast-agent://agents/{self._name}/",
supported_interfaces=[
AgentInterface(
url=f"fast-agent://agents/{self._name}/",
protocol_binding="fast-agent",
)
],
version="0.1",
capabilities=DEFAULT_CAPABILITIES,
# TODO -- get these from the _llm
Expand Down
30 changes: 28 additions & 2 deletions src/fast_agent/agents/mcp_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)

import mcp
from a2a.types import AgentCard, AgentSkill
from a2a.types import AgentCard, AgentInterface, AgentSkill
from mcp.types import (
CallToolResult,
ContentBlock,
Expand Down Expand Up @@ -489,6 +489,12 @@ def _local_filesystem_runtime(self) -> LocalFilesystemRuntime | None:
return fallback
return None

def _consume_pending_media_attachments(self) -> list[ContentBlock]:
local_runtime = self._local_filesystem_runtime()
if local_runtime is None:
return []
return local_runtime.consume_pending_media_attachments()

@property
def has_filesystem_read_text_file_tool(self) -> bool:
"""Whether the active filesystem runtime currently exposes read_text_file."""
Expand Down Expand Up @@ -648,6 +654,12 @@ def _shell_read_text_file_enabled(self) -> bool:
return True
return self._context.config.shell_execution.enable_read_text_file

def _shell_attach_media_mode(self) -> Literal["auto", "on", "off"]:
"""Return whether shell-enabled agents should expose local attach_media."""
if not self._context or not self._context.config:
return "auto"
return self._context.config.shell_execution.enable_attach_media

def _resolve_shell_edit_tool_mode(self) -> Literal["write_text_file", "apply_patch", "off"]:
"""Return which shell edit tool should be exposed for the current model/config."""
default_mode: Literal["write_text_file", "apply_patch"]
Expand Down Expand Up @@ -713,16 +725,20 @@ def _maybe_enable_local_filesystem_runtime(self, working_directory: Path | None
enable_write = edit_mode == "write_text_file"
enable_apply_patch = edit_mode == "apply_patch"
enable_edit_file = edit_mode == "write_text_file"
enable_attach_media = self._shell_attach_media_mode()
model_info = self.llm.model_info if self.llm else None
local_runtime = self._local_filesystem_runtime()
if local_runtime is not None:
if working_directory is not None:
local_runtime.set_working_directory(working_directory)
local_runtime.set_model_info(model_info)
local_runtime.set_tool_handler_resolver(self._get_tool_handler)
local_runtime.set_enabled_tools(
enable_read=enable_read,
enable_write=enable_write,
enable_apply_patch=enable_apply_patch,
enable_edit_file=enable_edit_file,
enable_attach_media=enable_attach_media,
)
return

Expand All @@ -736,6 +752,8 @@ def _maybe_enable_local_filesystem_runtime(self, working_directory: Path | None
enable_write=enable_write,
enable_apply_patch=enable_apply_patch,
enable_edit_file=enable_edit_file,
enable_attach_media=enable_attach_media,
model_info=model_info,
tool_handler_resolver=self._get_tool_handler,
)
if self._filesystem_runtime is None:
Expand All @@ -752,6 +770,7 @@ def _maybe_enable_local_filesystem_runtime(self, working_directory: Path | None
write_enabled=enable_write,
apply_patch_enabled=enable_apply_patch,
edit_file_enabled=enable_edit_file,
attach_media_enabled=enable_attach_media,
)

def _shell_output_limit_overridden(self) -> bool:
Expand Down Expand Up @@ -779,11 +798,13 @@ def _on_llm_attached(self, llm: FastAgentLLMProtocol) -> None:
local_runtime = self._local_filesystem_runtime()
if local_runtime is not None:
edit_mode = self._resolve_shell_edit_tool_mode()
local_runtime.set_model_info(llm.model_info)
local_runtime.set_enabled_tools(
enable_read=self._shell_read_text_file_enabled(),
enable_write=edit_mode == "write_text_file",
enable_apply_patch=edit_mode == "apply_patch",
enable_edit_file=edit_mode == "write_text_file",
enable_attach_media=self._shell_attach_media_mode(),
)

if self._shell_runtime is None:
Expand Down Expand Up @@ -2032,7 +2053,12 @@ async def agent_card(self) -> AgentCard:
skills=skills,
name=self._name,
description=self.config.description or self.instruction,
url=f"fast-agent://agents/{self._name}/",
supported_interfaces=[
AgentInterface(
url=f"fast-agent://agents/{self._name}/",
protocol_binding="fast-agent",
)
],
version="0.1",
capabilities=DEFAULT_CAPABILITIES,
default_input_modes=["text/plain"],
Expand Down
3 changes: 2 additions & 1 deletion src/fast_agent/agents/smart_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1592,7 +1592,8 @@ def _enable_smart_tooling(agent: _SmartToolingAgent) -> None:
description=(
"Run subagent tasks from a definition in a file or directory. Subagents are defined with "
"AgentCards. Use action=`run` to load a subagent and send it a message. Optionally supply "
"`mcp_connect` targets. Use action=`validate` to check card file validity without running them"
"`mcp_connect` targets. Use action=`validate` to check card file validity without running them. "
"Do not pass Agent Skill SKILL.md files here; inspect skills with read_text_file/read_skill."
),
)
slash_command_tool = build_default_function_tool(
Expand Down
17 changes: 14 additions & 3 deletions src/fast_agent/agents/tool_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
from typing import Any, Callable, Dict, List, Mapping, Sequence

from fastmcp.tools import FunctionTool, ToolResult
from mcp.types import CallToolResult, ListToolsResult, Tool
from mcp.types import CallToolResult, ContentBlock, ListToolsResult, Tool

from fast_agent.agents.agent_types import AgentConfig, AgentType
from fast_agent.agents.llm_agent import LlmAgent
from fast_agent.agents.tool_runner import ToolRunner, ToolRunnerHooks, _ToolLoopAgent
from fast_agent.constants import (
FAST_AGENT_ERROR_CHANNEL,
FAST_AGENT_PENDING_MEDIA_ATTACHMENTS,
FAST_AGENT_TOOL_METADATA,
HUMAN_INPUT_TOOL_NAME,
should_parallelize_tool_calls,
Expand Down Expand Up @@ -607,6 +608,10 @@ def should_suppress_tools_for_structured_turn(
def _should_display_user_message(self, message: PromptMessageExtended) -> bool:
return not message.tool_results

def _consume_pending_media_attachments(self) -> list[ContentBlock]:
"""Return pending media blocks to send as the next user input."""
return []

# we take care of tool results, so skip displaying them
def show_user_message(self, message: PromptMessageExtended) -> None:
if message.tool_results:
Expand Down Expand Up @@ -810,8 +815,8 @@ def _finalize_tool_results(
)
from fast_agent.mcp.url_elicitation_required import URLElicitationRequiredDisplayPayload

channels = None
content = []
channels: dict[str, Sequence[ContentBlock]] | None = None
content: list[ContentBlock] = []
if tool_loop_error:
content.append(text_content(tool_loop_error))
channels = {
Expand Down Expand Up @@ -846,6 +851,12 @@ def _finalize_tool_results(
TextContent(type="text", text=json.dumps(deferred_url_elicitations))
]

pending_media = self._consume_pending_media_attachments()
if pending_media:
if channels is None:
channels = {}
channels[FAST_AGENT_PENDING_MEDIA_ATTACHMENTS] = pending_media

return PromptMessageExtended(
role="user",
content=content,
Expand Down
16 changes: 14 additions & 2 deletions src/fast_agent/agents/tool_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fast_agent.constants import (
DEFAULT_MAX_ITERATIONS,
FAST_AGENT_ERROR_CHANNEL,
FAST_AGENT_PENDING_MEDIA_ATTACHMENTS,
FAST_AGENT_SYNTHETIC_FINAL_CHANNEL,
FAST_AGENT_TIMING,
FAST_AGENT_USAGE,
Expand Down Expand Up @@ -582,12 +583,23 @@ def has_pending_tool_response(self) -> bool:
return self._pending_tool_request is not None

def _stage_tool_response(self, tool_message: PromptMessageExtended) -> None:
staged_messages = [tool_message]
channels = tool_message.channels
if channels and FAST_AGENT_PENDING_MEDIA_ATTACHMENTS in channels:
pending_media = channels[FAST_AGENT_PENDING_MEDIA_ATTACHMENTS]
visible_channels = dict(channels)
del visible_channels[FAST_AGENT_PENDING_MEDIA_ATTACHMENTS]
staged_messages = [
tool_message.model_copy(update={"channels": visible_channels or None}),
PromptMessageExtended(role="user", content=list(pending_media)),
]

if self._use_history_enabled():
self._delta_messages = [tool_message]
self._delta_messages = staged_messages
else:
if self._last_message is not None:
self._delta_messages.append(self._last_message)
self._delta_messages.append(tool_message)
self._delta_messages.extend(staged_messages)

def _should_start_deferred_structured_finalization(
self,
Expand Down
12 changes: 10 additions & 2 deletions src/fast_agent/agents/workflow/router_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
by determining the best agent for a request and dispatching to it.
"""

import json
from typing import TYPE_CHECKING, List, Optional, Tuple, Type

from google.protobuf.json_format import MessageToDict
from mcp import Tool
from opentelemetry import trace
from pydantic import BaseModel
Expand Down Expand Up @@ -165,9 +167,15 @@ async def _generate_routing_instruction(
agent_descriptions = []
for agent in agents:
agent_card: AgentCard = await agent.agent_card()
card_summary = {
"name": agent_card.name,
"description": agent_card.description,
"skills": [MessageToDict(skill) for skill in agent_card.skills],
}
agent_descriptions.append(
agent_card.model_dump_json(
include={"name", "description", "skills"}, exclude_none=True
json.dumps(
{key: value for key, value in card_summary.items() if value},
separators=(",", ":"),
)
)

Expand Down
Loading
Loading