From 4e5444646ed656cc3f18d84fde9ac88e46a7efaf Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Fri, 18 Jul 2025 15:59:30 +0800 Subject: [PATCH 1/7] fix(common): Improve constant SEP from "-" to "__" for namespaced names - Modify test cases to include prompt testing --- src/mcp_agent/mcp/common.py | 2 +- tests/integration/api/mcp_tools_server.py | 11 +++++++++-- tests/integration/api/test_hyphens_in_name.py | 13 +++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/mcp_agent/mcp/common.py b/src/mcp_agent/mcp/common.py index 5935dc463..a62e298fe 100644 --- a/src/mcp_agent/mcp/common.py +++ b/src/mcp_agent/mcp/common.py @@ -3,7 +3,7 @@ """ # Constants -SEP = "-" +SEP = "__" def create_namespaced_name(server_name: str, resource_name: str) -> str: diff --git a/tests/integration/api/mcp_tools_server.py b/tests/integration/api/mcp_tools_server.py index 9c56db84f..05ffbb79c 100644 --- a/tests/integration/api/mcp_tools_server.py +++ b/tests/integration/api/mcp_tools_server.py @@ -13,12 +13,19 @@ # Create the FastMCP server app = FastMCP(name="An MCP Server", instructions="Here is how to use this server") +@app.prompt( + name="check_weather_prompt", + description="Asks for the weather in a specified location.", +) +def check_weather_prompt(location: str) -> str: + """The location to check""" + return f"Check the weather in {location}" @app.tool( - name="check_weather", + name="check_weather_tool", description="Returns the weather for a specified location.", ) -def check_weather(location: str) -> str: +def check_weather_tool(location: str) -> str: """The location to check""" # Write the location to a text file with open("weather_location.txt", "w") as f: diff --git a/tests/integration/api/test_hyphens_in_name.py b/tests/integration/api/test_hyphens_in_name.py index e002197e2..4e99c7057 100644 --- a/tests/integration/api/test_hyphens_in_name.py +++ b/tests/integration/api/test_hyphens_in_name.py @@ -1,6 +1,7 @@ - import pytest +from src.mcp_agent.mcp.common import SEP + @pytest.mark.integration @pytest.mark.asyncio @@ -10,7 +11,15 @@ async def test_hyphenated_server_name(fast_agent): @fast.agent(name="test", instruction="here are you instructions", servers=["hyphen-test"]) async def agent_function(): async with fast.run() as app: - result = await app.test.send('***CALL_TOOL check_weather {"location": "New York"}') + # test prompt/get request + get_prompt_result = await app.test.get_prompt( + prompt_name=f"hyphen-test{SEP}check_weather_prompt", + arguments={"location": "New York"}, + ) + assert get_prompt_result.description is None + + # test tool calling + result = await app.test.send('***CALL_TOOL check_weather_tool {"location": "New York"}') assert "sunny" in result await agent_function() From d9e227452fb5c3ed9b7b12a08357625413a4a54e Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Fri, 18 Jul 2025 17:21:21 +0800 Subject: [PATCH 2/7] fix(#298): Improve some hard-coded SEP value in tests --- tests/integration/api/test_describe_a2a.py | 4 +++- tests/integration/api/test_hyphens_in_name.py | 2 +- tests/integration/api/test_tool_list_change.py | 6 ++++-- tests/integration/roots/live.py | 3 ++- tests/integration/roots/test_roots.py | 4 +++- tests/integration/sampling/live.py | 5 +++-- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/integration/api/test_describe_a2a.py b/tests/integration/api/test_describe_a2a.py index d2d212668..3b707d0e2 100644 --- a/tests/integration/api/test_describe_a2a.py +++ b/tests/integration/api/test_describe_a2a.py @@ -2,6 +2,8 @@ import pytest +from mcp_agent.mcp.common import SEP + if TYPE_CHECKING: from a2a_types.types import AgentCard, AgentSkill @@ -26,7 +28,7 @@ async def agent_function(): assert 2 == len(card.skills) skill: AgentSkill = card.skills[0] - assert "card_test-check_weather" == skill.id + assert f"card_test{SEP}check_weather" == skill.id assert "check_weather" == skill.name assert "Returns the weather for a specified location." assert skill.tags diff --git a/tests/integration/api/test_hyphens_in_name.py b/tests/integration/api/test_hyphens_in_name.py index 4e99c7057..175120eb3 100644 --- a/tests/integration/api/test_hyphens_in_name.py +++ b/tests/integration/api/test_hyphens_in_name.py @@ -1,6 +1,6 @@ import pytest -from src.mcp_agent.mcp.common import SEP +from mcp_agent.mcp.common import SEP @pytest.mark.integration diff --git a/tests/integration/api/test_tool_list_change.py b/tests/integration/api/test_tool_list_change.py index 108e59baa..8267e50b9 100644 --- a/tests/integration/api/test_tool_list_change.py +++ b/tests/integration/api/test_tool_list_change.py @@ -4,6 +4,8 @@ import pytest +from mcp_agent.mcp.common import SEP + if TYPE_CHECKING: from mcp import ListToolsResult @@ -24,7 +26,7 @@ async def agent_function(): # Initially there should be one tool (check_weather) tools: ListToolsResult = await app.test.list_tools() assert 1 == len(tools.tools) - assert "dynamic_tool-check_weather" == tools.tools[0].name + assert f"dynamic_tool{SEP}check_weather" == tools.tools[0].name # Calling check_weather will toggle the dynamic_tool and send a notification result = await app.test.send('***CALL_TOOL check_weather {"location": "New York"}') @@ -37,7 +39,7 @@ async def agent_function(): dynamic_tool_found = False # Check if dynamic_tool is in the list for tool in tools.tools: - if tool.name == "dynamic_tool-dynamic_tool": + if tool.name == f"dynamic_tool{SEP}dynamic_tool": dynamic_tool_found = True break diff --git a/tests/integration/roots/live.py b/tests/integration/roots/live.py index 89b88fd9a..1ca90bc36 100644 --- a/tests/integration/roots/live.py +++ b/tests/integration/roots/live.py @@ -1,6 +1,7 @@ import asyncio from mcp_agent.core.fastagent import FastAgent +from mcp_agent.mcp.common import SEP # Create the application fast = FastAgent("FastAgent Example") @@ -11,7 +12,7 @@ async def main(): # use the --model command line switch or agent arguments to change model async with fast.run() as agent: - await agent.send("***CALL_TOOL roots_test-show_roots {}") + await agent.send(f"***CALL_TOOL roots_test{SEP}show_roots {{}}") if __name__ == "__main__": diff --git a/tests/integration/roots/test_roots.py b/tests/integration/roots/test_roots.py index 2dddb9184..6760b391b 100644 --- a/tests/integration/roots/test_roots.py +++ b/tests/integration/roots/test_roots.py @@ -1,5 +1,7 @@ import pytest +from mcp_agent.mcp.common import SEP + @pytest.mark.integration @pytest.mark.asyncio @@ -12,7 +14,7 @@ async def test_roots_returned(fast_agent): @fast.agent(name="foo", instruction="bar", servers=["roots_test"]) async def agent_function(): async with fast.run() as agent: - result = await agent.foo.send("***CALL_TOOL roots_test-show_roots {}") + result = await agent.foo.send(f"***CALL_TOOL roots_test{SEP}show_roots {{}}") assert "file:///mnt/data/" in result # alias assert "test_data" in result assert "file://no/alias" in result # no alias. diff --git a/tests/integration/sampling/live.py b/tests/integration/sampling/live.py index 93bc577ae..534c19a31 100644 --- a/tests/integration/sampling/live.py +++ b/tests/integration/sampling/live.py @@ -1,6 +1,7 @@ import asyncio from mcp_agent.core.fastagent import FastAgent +from mcp_agent.mcp.common import SEP # Create the application with specified model fast = FastAgent("FastAgent Example") @@ -11,10 +12,10 @@ async def main(): # use the --model command line switch or agent arguments to change model async with fast.run() as agent: - result = await agent.send('***CALL_TOOL sampling_test-sample {"to_sample": "123foo"}') + result = await agent.send(f'***CALL_TOOL sampling_test{SEP}sample {"to_sample": "123foo"}') print(f"RESULT: {result}") - result = await agent.send('***CALL_TOOL slow_sampling-sample_parallel') + result = await agent.send(f'***CALL_TOOL slow_sampling{SEP}sample_parallel') print(f"RESULT: {result}") From b2c7f675c57efb00978027ac485ed7a02f86b3a2 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Fri, 18 Jul 2025 17:35:39 +0800 Subject: [PATCH 3/7] fix(#298): Change 'check_weather_tool' back to 'check_weather' to avoid affect other tests --- tests/integration/api/mcp_tools_server.py | 4 ++-- tests/integration/api/test_hyphens_in_name.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/api/mcp_tools_server.py b/tests/integration/api/mcp_tools_server.py index 05ffbb79c..03df63621 100644 --- a/tests/integration/api/mcp_tools_server.py +++ b/tests/integration/api/mcp_tools_server.py @@ -22,10 +22,10 @@ def check_weather_prompt(location: str) -> str: return f"Check the weather in {location}" @app.tool( - name="check_weather_tool", + name="check_weather", description="Returns the weather for a specified location.", ) -def check_weather_tool(location: str) -> str: +def check_weather(location: str) -> str: """The location to check""" # Write the location to a text file with open("weather_location.txt", "w") as f: diff --git a/tests/integration/api/test_hyphens_in_name.py b/tests/integration/api/test_hyphens_in_name.py index 175120eb3..59dce7eb6 100644 --- a/tests/integration/api/test_hyphens_in_name.py +++ b/tests/integration/api/test_hyphens_in_name.py @@ -19,7 +19,7 @@ async def agent_function(): assert get_prompt_result.description is None # test tool calling - result = await app.test.send('***CALL_TOOL check_weather_tool {"location": "New York"}') + result = await app.test.send('***CALL_TOOL check_weather {"location": "New York"}') assert "sunny" in result await agent_function() From 7c70d98fd3ac3309ba048b5bf6fbdaa8df01fd42 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:48:51 +0000 Subject: [PATCH 4/7] update --- examples/a2a/agent_executor.py | 2 +- src/fast_agent/agents/agent_types.py | 11 +- src/fast_agent/agents/llm_agent.py | 13 +- src/fast_agent/agents/mcp_agent.py | 221 +++++++----------- src/fast_agent/mcp/__init__.py | 3 + src/fast_agent/mcp/common.py | 10 + tests/integration/api/test_describe_a2a.py | 2 +- tests/integration/api/test_hyphens_in_name.py | 4 +- .../integration/api/test_tool_list_change.py | 14 +- .../mcp_filtering/test_mcp_filtering.py | 55 ++--- tests/integration/roots/live.py | 2 +- tests/integration/roots/test_roots.py | 2 +- tests/integration/sampling/live.py | 4 +- 13 files changed, 142 insertions(+), 201 deletions(-) diff --git a/examples/a2a/agent_executor.py b/examples/a2a/agent_executor.py index c8e57c677..f0722a65a 100644 --- a/examples/a2a/agent_executor.py +++ b/examples/a2a/agent_executor.py @@ -18,7 +18,7 @@ DEFAULT_AGENT_NAME = "helper" fast = FastAgent( - "A2A FastAgent Demo", + "A2A fast-agent Demo", parse_cli_args=False, quiet=True, ) diff --git a/src/fast_agent/agents/agent_types.py b/src/fast_agent/agents/agent_types.py index 4ff2c06c6..4902c0a67 100644 --- a/src/fast_agent/agents/agent_types.py +++ b/src/fast_agent/agents/agent_types.py @@ -5,7 +5,6 @@ from dataclasses import dataclass, field from enum import StrEnum, auto from pathlib import Path -from typing import Dict, List, Optional from mcp.client.session import ElicitationFnT @@ -35,12 +34,12 @@ class AgentConfig: name: str instruction: str = "You are a helpful agent." - servers: List[str] = field(default_factory=list) - tools: Optional[Dict[str, List[str]]] = None - resources: Optional[Dict[str, List[str]]] = None - prompts: Optional[Dict[str, List[str]]] = None + servers: list[str] = field(default_factory=list) + tools: dict[str, list[str]] = field(default_factory=dict) # filters for tools + resources: dict[str, list[str]] = field(default_factory=dict) # filters for resources + prompts: dict[str, list[str]] = field(default_factory=dict) # filters for prompts skills: SkillManifest | SkillRegistry | Path | str | None = None - skill_manifests: List[SkillManifest] = field(default_factory=list, repr=False) + skill_manifests: list[SkillManifest] = field(default_factory=list, repr=False) model: str | None = None use_history: bool = True default_request_params: RequestParams | None = None diff --git a/src/fast_agent/agents/llm_agent.py b/src/fast_agent/agents/llm_agent.py index acfd46e7f..f62d98497 100644 --- a/src/fast_agent/agents/llm_agent.py +++ b/src/fast_agent/agents/llm_agent.py @@ -10,18 +10,7 @@ from typing import Callable, List, Optional, Tuple -try: - from a2a.types import AgentCapabilities # type: ignore -except Exception: # pragma: no cover - optional dependency fallback - from dataclasses import dataclass - - @dataclass - class AgentCapabilities: # minimal fallback - streaming: bool = False - push_notifications: bool = False - state_transition_history: bool = False - - +from a2a.types import AgentCapabilities from mcp import Tool from rich.text import Text diff --git a/src/fast_agent/agents/mcp_agent.py b/src/fast_agent/agents/mcp_agent.py index 1a6bea2e4..483639b0f 100644 --- a/src/fast_agent/agents/mcp_agent.py +++ b/src/fast_agent/agents/mcp_agent.py @@ -41,7 +41,7 @@ from fast_agent.core.exceptions import PromptExitError from fast_agent.core.logging.logger import get_logger from fast_agent.interfaces import FastAgentLLMProtocol -from fast_agent.mcp.common import SEP +from fast_agent.mcp.common import get_resource_name, get_server_name, is_namespaced_name from fast_agent.mcp.mcp_aggregator import MCPAggregator, ServerStatus from fast_agent.skills.registry import format_skills_for_prompt from fast_agent.tools.elicitation import ( @@ -345,104 +345,42 @@ async def __call__( ) -> str: return await self.send(message) - # async def send( - # self, - # message: Union[ - # str, - # PromptMessage, - # PromptMessageExtended, - # Sequence[Union[str, PromptMessage, PromptMessageExtended]], - # ], - # request_params: RequestParams | None = None, - # ) -> str: - # """ - # Send a message to the agent and get a response. - - # Args: - # message: Message content in various formats: - # - String: Converted to a user PromptMessageExtended - # - PromptMessage: Converted to PromptMessageExtended - # - PromptMessageExtended: Used directly - # - request_params: Optional request parameters - - # Returns: - # The agent's response as a string - # """ - # response = await self.generate(message, request_params) - # return response.last_text() or "" - - def _matches_pattern(self, name: str, pattern: str, server_name: str) -> bool: + def _matches_pattern(self, name: str, pattern: str) -> bool: """ Check if a name matches a pattern for a specific server. Args: name: The name to match (could be tool name, resource URI, or prompt name) pattern: The pattern to match against (e.g., "add", "math*", "resource://math/*") - server_name: The server name (used for tool name prefixing) Returns: True if the name matches the pattern """ - # For tools, build the full pattern with server prefix: server_name-pattern - if name.startswith(f"{server_name}-"): - full_pattern = f"{server_name}-{pattern}" - return fnmatch.fnmatch(name, full_pattern) # For resources and prompts, match directly against the pattern return fnmatch.fnmatch(name, pattern) - async def list_tools(self) -> ListToolsResult: + def _filter_tools_by_config(self, tools: Sequence[Tool] | None) -> list[Tool]: """ - List all tools available to this agent, filtered by configuration. - - Returns: - ListToolsResult with available tools + Apply configuration-based filtering to a collection of tools. """ - aggregator_result = await self._aggregator.list_tools() - aggregator_tools = list(aggregator_result.tools or []) - - # Apply filtering if tools are specified in config - if self.config.tools is not None: - filtered_tools: list[Tool] = [] - for tool in aggregator_tools: - # Extract server name from tool name, handling server names with hyphens - server_name = None - for configured_server in self.config.tools.keys(): - if tool.name.startswith(f"{configured_server}{SEP}"): - server_name = configured_server - break - - if not server_name: - continue - - # Check if tool matches any pattern for this server - for pattern in self.config.tools[server_name]: - if self._matches_pattern(tool.name, pattern, server_name): - filtered_tools.append(tool) - break - aggregator_tools = filtered_tools - - # Start with filtered aggregator tools and merge in subclass/local tools - merged_tools: list[Tool] = list(aggregator_tools) - existing_names = {tool.name for tool in merged_tools} + if not tools: + return [] - local_tools = (await ToolAgent.list_tools(self)).tools - for tool in local_tools: - if tool.name not in existing_names: - merged_tools.append(tool) - existing_names.add(tool.name) - - if self._bash_tool and self._bash_tool.name not in existing_names: - merged_tools.append(self._bash_tool) - existing_names.add(self._bash_tool.name) + return [ + tool + for tool in tools + if is_namespaced_name(tool.name) and self._tool_matches_filter(tool.name) + ] - if self.config.human_input: - human_tool = getattr(self, "_human_input_tool", None) - if human_tool and human_tool.name not in existing_names: - merged_tools.append(human_tool) - existing_names.add(human_tool.name) + def _filter_tools(self, tools: Sequence[Tool] | None) -> list[Tool]: + """ + Filter items for a specific server (not namespaced) + """ + if not tools: + return [] - return ListToolsResult(tools=merged_tools) + return [tool for tool in tools if self._tool_matches_filter(tool.name)] async def call_tool(self, name: str, arguments: Dict[str, Any] | None = None) -> CallToolResult: """ @@ -860,37 +798,6 @@ async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_nam with self._tracer.start_as_current_span(f"Agent: '{self._name}' apply_prompt_template"): return await self._llm.apply_prompt_template(prompt_result, prompt_name) - # async def structured( - # self, - # messages: Union[ - # str, - # PromptMessage, - # PromptMessageExtended, - # Sequence[Union[str, PromptMessage, PromptMessageExtended]], - # ], - # model: Type[ModelT], - # request_params: RequestParams | None = None, - # ) -> Tuple[ModelT | None, PromptMessageExtended]: - # """ - # Apply the prompt and return the result as a Pydantic model. - # Normalizes input messages and delegates to the attached LLM. - - # Args: - # messages: Message(s) in various formats: - # - String: Converted to a user PromptMessageExtended - # - PromptMessage: Converted to PromptMessageExtended - # - PromptMessageExtended: Used directly - # - List of any combination of the above - # model: The Pydantic model class to parse the result into - # request_params: Optional parameters to configure the LLM request - - # Returns: - # An instance of the specified model, or None if coercion fails - # """ - - # with self._tracer.start_as_current_span(f"Agent: '{self._name}' structured"): - # return await super().structured(messages, model, request_params) - async def apply_prompt_messages( self, prompts: List[PromptMessageExtended], request_params: RequestParams | None = None ) -> str: @@ -934,7 +841,7 @@ async def list_prompts( for prompt in prompts: # Check if prompt matches any pattern for this server for pattern in self.config.prompts[server]: - if self._matches_pattern(prompt.name, pattern, server): + if self._matches_pattern(prompt.name, pattern): filtered_prompts.append(prompt) break if filtered_prompts: @@ -969,7 +876,7 @@ async def list_resources( for resource in resources: # Check if resource matches any pattern for this server for pattern in self.config.resources[server]: - if self._matches_pattern(resource, pattern, server): + if self._matches_pattern(resource, pattern): filtered_resources.append(resource) break if filtered_resources: @@ -978,9 +885,7 @@ async def list_resources( return result - async def list_mcp_tools( - self, namespace: str | None = None, server_name: str | None = None - ) -> Mapping[str, List[Tool]]: + async def list_mcp_tools(self, namespace: str | None = None) -> Mapping[str, List[Tool]]: """ List all tools available to this agent, grouped by server and filtered by configuration. @@ -991,40 +896,72 @@ async def list_mcp_tools( Dictionary mapping server names to lists of Tool objects (with original names, not namespaced) """ # Get all tools from the aggregator - target = namespace if namespace is not None else server_name - result = await self._aggregator.list_mcp_tools(target) + result = await self._aggregator.list_mcp_tools(namespace) + filtered_result: dict[str, list[Tool]] = {} - # Apply filtering if tools are specified in config - if self.config.tools is not None: - filtered_result = {} - for server, tools in result.items(): - # Check if this server has tool filters - if server in self.config.tools: - filtered_tools = [] - for tool in tools: - # Check if tool matches any pattern for this server - for pattern in self.config.tools[server]: - if self._matches_pattern(tool.name, pattern, server): - filtered_tools.append(tool) - break - if filtered_tools: - filtered_result[server] = filtered_tools - result = filtered_result + for server, server_tools in result.items(): + filtered_result[server] = self._filter_tools_by_config(server_tools) # Add elicitation-backed human input tool to a special server if enabled and available - if self.config.human_input and getattr(self, "_human_input_tool", None): + if self.config.human_input and self._human_input_tool: special_server_name = "__human_input__" + filtered_result.setdefault(special_server_name, []).append(self._human_input_tool) - # If the special server doesn't exist in result, create it - if special_server_name not in result: - result[special_server_name] = [] + return filtered_result - result[special_server_name].append(self._human_input_tool) + async def list_tools(self) -> ListToolsResult: + """ + List all tools available to this agent, filtered by configuration. - # if self._skill_lookup_tool: - # result.setdefault("__skills__", []).append(self._skill_lookup_tool) + Returns: + ListToolsResult with available tools + """ + # Start with filtered aggregator tools and merge in subclass/local tools + merged_tools: list[Tool] = await self._get_filtered_tools() + existing_names = {tool.name for tool in merged_tools} - return result + local_tools = (await super().list_tools()).tools + for tool in local_tools: + if tool.name not in existing_names: + merged_tools.append(tool) + existing_names.add(tool.name) + + if self._bash_tool and self._bash_tool.name not in existing_names: + merged_tools.append(self._bash_tool) + existing_names.add(self._bash_tool.name) + + if self.config.human_input: + human_tool = getattr(self, "_human_input_tool", None) + if human_tool and human_tool.name not in existing_names: + merged_tools.append(human_tool) + existing_names.add(human_tool.name) + + return ListToolsResult(tools=merged_tools) + + async def _get_filtered_tools(self) -> list[Tool]: + """ + Get the list of tools available to this agent, filtered by configuration. + + Returns: + List of Tool objects + """ + aggregator_result = await self._aggregator.list_tools() + return self._filter_tools_by_config(getattr(aggregator_result, "tools", None)) + + def _tool_matches_filter(self, tool_name: str) -> bool: + """ + Check if a tool name matches the agent's tool configuration. + + Args: + tool_name: The name of the tool to check (namespaced) + """ + server_name = get_server_name(tool_name) + config_tools = self.config.tools or {} + if server_name not in config_tools: + return True + resource_name = get_resource_name(tool_name) + patterns = config_tools.get(server_name, []) + return any(self._matches_pattern(resource_name, pattern) for pattern in patterns) @property def agent_type(self) -> AgentType: diff --git a/src/fast_agent/mcp/__init__.py b/src/fast_agent/mcp/__init__.py index 8243e52ee..825337ab2 100644 --- a/src/fast_agent/mcp/__init__.py +++ b/src/fast_agent/mcp/__init__.py @@ -10,6 +10,7 @@ via `fast_agent.mcp.prompt_message_multipart`, which subclasses `PromptMessageExtended`. """ +from .common import SEP from .helpers import ( ensure_multipart_messages, get_image_data, @@ -27,6 +28,8 @@ __all__ = [ "Prompt", + # Common + "SEP", # Helpers "get_text", "get_image_data", diff --git a/src/fast_agent/mcp/common.py b/src/fast_agent/mcp/common.py index a62e298fe..a1ed5d88f 100644 --- a/src/fast_agent/mcp/common.py +++ b/src/fast_agent/mcp/common.py @@ -14,3 +14,13 @@ def create_namespaced_name(server_name: str, resource_name: str) -> str: def is_namespaced_name(name: str) -> bool: """Check if a name is already namespaced""" return SEP in name + + +def get_server_name(namespaced_name: str) -> str: + """Extract the server name from a namespaced resource name""" + return namespaced_name.split(SEP)[0] if SEP in namespaced_name else "" + + +def get_resource_name(namespaced_name: str) -> str: + """Extract the resource name from a namespaced resource name""" + return namespaced_name.split(SEP, 1)[1] if SEP in namespaced_name else namespaced_name diff --git a/tests/integration/api/test_describe_a2a.py b/tests/integration/api/test_describe_a2a.py index f80f4476c..6dfadc333 100644 --- a/tests/integration/api/test_describe_a2a.py +++ b/tests/integration/api/test_describe_a2a.py @@ -2,7 +2,7 @@ import pytest -from mcp_agent.mcp.common import SEP +from fast_agent.mcp import SEP if TYPE_CHECKING: from a2a.types import AgentCard, AgentSkill diff --git a/tests/integration/api/test_hyphens_in_name.py b/tests/integration/api/test_hyphens_in_name.py index 36565686e..794dab7e4 100644 --- a/tests/integration/api/test_hyphens_in_name.py +++ b/tests/integration/api/test_hyphens_in_name.py @@ -1,6 +1,6 @@ import pytest -from mcp_agent.mcp.common import SEP +from fast_agent.mcp import SEP @pytest.mark.integration @@ -16,7 +16,7 @@ async def agent_function(): prompt_name=f"hyphen-test{SEP}check_weather_prompt", arguments={"location": "New York"}, ) - assert get_prompt_result.description is None + assert get_prompt_result.description # test tool calling result = await app.test.send('***CALL_TOOL check_weather {"location": "New York"}') diff --git a/tests/integration/api/test_tool_list_change.py b/tests/integration/api/test_tool_list_change.py index 109b14e2a..c34bf48a6 100644 --- a/tests/integration/api/test_tool_list_change.py +++ b/tests/integration/api/test_tool_list_change.py @@ -4,9 +4,11 @@ import pytest -from mcp_agent.mcp.common import SEP +from fast_agent.mcp import SEP if TYPE_CHECKING: + from mcp import ListToolsResult + from fast_agent.mcp.mcp_aggregator import NamespacedTool # Enable debug logging for the test @@ -42,11 +44,11 @@ async def agent_function(): tools_dict = await app.test.list_mcp_tools() dynamic_tool_found = False # Check if dynamic_tool is in the list - for tool in tools.tools: - if tool.name == f"dynamic_tool{SEP}dynamic_tool": - dynamic_tool_found = True - break - + if "dynamic_tool" in tools_dict: + for tool in tools_dict["dynamic_tool"]: + if tool.name == "dynamic_tool": + dynamic_tool_found = True + break # Verify the dynamic tool was added assert dynamic_tool_found, ( "Dynamic tool was not added to the tool list after notification" diff --git a/tests/integration/mcp_filtering/test_mcp_filtering.py b/tests/integration/mcp_filtering/test_mcp_filtering.py index afd8d9b25..cf12d3edd 100644 --- a/tests/integration/mcp_filtering/test_mcp_filtering.py +++ b/tests/integration/mcp_filtering/test_mcp_filtering.py @@ -7,6 +7,7 @@ import pytest from fast_agent.agents import McpAgent +from fast_agent.mcp.common import SEP @pytest.mark.integration @@ -30,13 +31,13 @@ async def agent_no_filter(): # Should have all 7 tools expected_tools = { - "filtering_test_server-math_add", - "filtering_test_server-math_subtract", - "filtering_test_server-math_multiply", - "filtering_test_server-string_upper", - "filtering_test_server-string_lower", - "filtering_test_server-utility_ping", - "filtering_test_server-utility_status", + f"filtering_test_server{SEP}math_add", + f"filtering_test_server{SEP}math_subtract", + f"filtering_test_server{SEP}math_multiply", + f"filtering_test_server{SEP}string_upper", + f"filtering_test_server{SEP}string_lower", + f"filtering_test_server{SEP}utility_ping", + f"filtering_test_server{SEP}utility_status", } actual_tools = set(tool_names) assert actual_tools == expected_tools, f"Expected {expected_tools}, got {actual_tools}" @@ -58,19 +59,19 @@ async def agent_with_filter(): # Should have only math tools + string_upper expected_tools = { - "filtering_test_server-math_add", - "filtering_test_server-math_subtract", - "filtering_test_server-math_multiply", - "filtering_test_server-string_upper", + f"filtering_test_server{SEP}math_add", + f"filtering_test_server{SEP}math_subtract", + f"filtering_test_server{SEP}math_multiply", + f"filtering_test_server{SEP}string_upper", } actual_tools = set(tool_names) assert actual_tools == expected_tools, f"Expected {expected_tools}, got {actual_tools}" # Should NOT have these tools excluded_tools = { - "filtering_test_server-string_lower", - "filtering_test_server-utility_ping", - "filtering_test_server-utility_status", + f"filtering_test_server{SEP}string_lower", + f"filtering_test_server{SEP}utility_ping", + f"filtering_test_server{SEP}utility_status", } for tool_name in excluded_tools: assert tool_name not in tool_names, ( @@ -101,9 +102,9 @@ async def elapsed_agent(): ) elapsed = getattr(result, "transport_elapsed", None) - assert ( - elapsed is not None - ), "transport_elapsed should be attached to MCP CallToolResult responses" + assert elapsed is not None, ( + "transport_elapsed should be attached to MCP CallToolResult responses" + ) assert elapsed >= 0, "elapsed time should never be negative" assert elapsed < 120, "elapsed time should be within a credible bound" @@ -267,19 +268,19 @@ async def custom_string_agent(): # Should have only string tools expected_tools = { - "filtering_test_server-string_upper", - "filtering_test_server-string_lower", + f"filtering_test_server{SEP}string_upper", + f"filtering_test_server{SEP}string_lower", } actual_tools = set(tool_names) assert actual_tools == expected_tools, f"Expected {expected_tools}, got {actual_tools}" # Should NOT have math or utility tools excluded_tools = { - "filtering_test_server-math_add", - "filtering_test_server-math_subtract", - "filtering_test_server-math_multiply", - "filtering_test_server-utility_ping", - "filtering_test_server-utility_status", + f"filtering_test_server{SEP}math_add", + f"filtering_test_server{SEP}math_subtract", + f"filtering_test_server{SEP}math_multiply", + f"filtering_test_server{SEP}utility_ping", + f"filtering_test_server{SEP}utility_status", } for tool_name in excluded_tools: assert tool_name not in tool_names, ( @@ -311,9 +312,9 @@ async def agent_combined_filter(): tools = await agent_app.agent_combined_filter.list_tools() tool_names = [tool.name for tool in tools.tools] expected_tools = { - "filtering_test_server-math_add", - "filtering_test_server-math_subtract", - "filtering_test_server-math_multiply", + f"filtering_test_server{SEP}math_add", + f"filtering_test_server{SEP}math_subtract", + f"filtering_test_server{SEP}math_multiply", } actual_tools = set(tool_names) assert actual_tools == expected_tools, ( diff --git a/tests/integration/roots/live.py b/tests/integration/roots/live.py index d0b1a6843..7c91ad20b 100644 --- a/tests/integration/roots/live.py +++ b/tests/integration/roots/live.py @@ -1,6 +1,6 @@ import asyncio -from fast_agent import FastAgent +from fast_agent.mcp import SEP, FastAgent # Create the application fast = FastAgent("FastAgent Example") diff --git a/tests/integration/roots/test_roots.py b/tests/integration/roots/test_roots.py index 6760b391b..405d59972 100644 --- a/tests/integration/roots/test_roots.py +++ b/tests/integration/roots/test_roots.py @@ -1,6 +1,6 @@ import pytest -from mcp_agent.mcp.common import SEP +from fast_agent.mcp import SEP @pytest.mark.integration diff --git a/tests/integration/sampling/live.py b/tests/integration/sampling/live.py index cadba6359..27a69a337 100644 --- a/tests/integration/sampling/live.py +++ b/tests/integration/sampling/live.py @@ -1,9 +1,9 @@ import asyncio -from fast_agent import FastAgent +from fast_agent.mcp import SEP, FastAgent # Create the application with specified model -fast = FastAgent("FastAgent Example") +fast = FastAgent("fast-agent Example") # Define the agent From 78e71b5d2c0cd4a92489daec0df9b001b11ab3eb Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:08:16 +0000 Subject: [PATCH 5/7] filtering improvements --- src/fast_agent/agents/mcp_agent.py | 71 +++++++++++++++++------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/fast_agent/agents/mcp_agent.py b/src/fast_agent/agents/mcp_agent.py index 483639b0f..8e992c79c 100644 --- a/src/fast_agent/agents/mcp_agent.py +++ b/src/fast_agent/agents/mcp_agent.py @@ -360,7 +360,7 @@ def _matches_pattern(self, name: str, pattern: str) -> bool: # For resources and prompts, match directly against the pattern return fnmatch.fnmatch(name, pattern) - def _filter_tools_by_config(self, tools: Sequence[Tool] | None) -> list[Tool]: + def _filter_namespaced_tools(self, tools: Sequence[Tool] | None) -> list[Tool]: """ Apply configuration-based filtering to a collection of tools. """ @@ -373,14 +373,48 @@ def _filter_tools_by_config(self, tools: Sequence[Tool] | None) -> list[Tool]: if is_namespaced_name(tool.name) and self._tool_matches_filter(tool.name) ] - def _filter_tools(self, tools: Sequence[Tool] | None) -> list[Tool]: + def _filter_server_tools(self, tools: list[Tool] | None, namespace: str) -> list[Tool]: """ - Filter items for a specific server (not namespaced) + Filter items for a Server (not namespaced) """ if not tools: return [] - return [tool for tool in tools if self._tool_matches_filter(tool.name)] + filters = self.config.tools or {} + if namespace not in filters: + return tools + + patterns = filters.get(namespace, []) + return [ + tool + for tool in tools + if any(self._matches_pattern(tool.name, pattern) for pattern in patterns) + ] + + async def _get_filtered_mcp_tools(self) -> list[Tool]: + """ + Get the list of tools available to this agent, applying configured filters. + + Returns: + List of Tool objects + """ + aggregator_result = await self._aggregator.list_tools() + return self._filter_namespaced_tools(aggregator_result.tools) + + def _tool_matches_filter(self, packed_name: str) -> bool: + """ + Check if a tool name matches the agent's tool configuration. + + Args: + tool_name: The name of the tool to check (namespaced) + """ + server_name = get_server_name(packed_name) + config_tools = self.config.tools or {} + if server_name not in config_tools: + return True + resource_name = get_resource_name(packed_name) + patterns = config_tools.get(server_name, []) + return any(self._matches_pattern(resource_name, pattern) for pattern in patterns) async def call_tool(self, name: str, arguments: Dict[str, Any] | None = None) -> CallToolResult: """ @@ -900,7 +934,7 @@ async def list_mcp_tools(self, namespace: str | None = None) -> Mapping[str, Lis filtered_result: dict[str, list[Tool]] = {} for server, server_tools in result.items(): - filtered_result[server] = self._filter_tools_by_config(server_tools) + filtered_result[server] = self._filter_server_tools(server_tools, server) # Add elicitation-backed human input tool to a special server if enabled and available if self.config.human_input and self._human_input_tool: @@ -917,7 +951,7 @@ async def list_tools(self) -> ListToolsResult: ListToolsResult with available tools """ # Start with filtered aggregator tools and merge in subclass/local tools - merged_tools: list[Tool] = await self._get_filtered_tools() + merged_tools: list[Tool] = await self._get_filtered_mcp_tools() existing_names = {tool.name for tool in merged_tools} local_tools = (await super().list_tools()).tools @@ -938,31 +972,6 @@ async def list_tools(self) -> ListToolsResult: return ListToolsResult(tools=merged_tools) - async def _get_filtered_tools(self) -> list[Tool]: - """ - Get the list of tools available to this agent, filtered by configuration. - - Returns: - List of Tool objects - """ - aggregator_result = await self._aggregator.list_tools() - return self._filter_tools_by_config(getattr(aggregator_result, "tools", None)) - - def _tool_matches_filter(self, tool_name: str) -> bool: - """ - Check if a tool name matches the agent's tool configuration. - - Args: - tool_name: The name of the tool to check (namespaced) - """ - server_name = get_server_name(tool_name) - config_tools = self.config.tools or {} - if server_name not in config_tools: - return True - resource_name = get_resource_name(tool_name) - patterns = config_tools.get(server_name, []) - return any(self._matches_pattern(resource_name, pattern) for pattern in patterns) - @property def agent_type(self) -> AgentType: """ From 4a9fcbb99f370f8e07189acd71133aa136a2fe9b Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:32:54 +0000 Subject: [PATCH 6/7] improve filtering --- src/fast_agent/agents/mcp_agent.py | 95 ++++++++++++++++-------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/src/fast_agent/agents/mcp_agent.py b/src/fast_agent/agents/mcp_agent.py index 8e992c79c..ceb9e3166 100644 --- a/src/fast_agent/agents/mcp_agent.py +++ b/src/fast_agent/agents/mcp_agent.py @@ -11,6 +11,7 @@ from typing import ( TYPE_CHECKING, Any, + Callable, Dict, List, Mapping, @@ -55,6 +56,7 @@ # Define a TypeVar for models ModelT = TypeVar("ModelT", bound=BaseModel) +ItemT = TypeVar("ItemT") LLM = TypeVar("LLM", bound=FastAgentLLMProtocol) @@ -373,6 +375,38 @@ def _filter_namespaced_tools(self, tools: Sequence[Tool] | None) -> list[Tool]: if is_namespaced_name(tool.name) and self._tool_matches_filter(tool.name) ] + def _filter_server_collections( + self, + items_by_server: Mapping[str, Sequence[ItemT]], + filters: Mapping[str, Sequence[str]] | None, + value_getter: Callable[[ItemT], str], + ) -> dict[str, list[ItemT]]: + """ + Apply server-specific filters to a mapping of collections. + """ + if not items_by_server: + return {} + + if not filters: + return {server: list(items) for server, items in items_by_server.items()} + + filtered: dict[str, list[ItemT]] = {} + for server, items in items_by_server.items(): + patterns = filters.get(server) + if patterns is None: + filtered[server] = list(items) + continue + + matches = [ + item + for item in items + if any(self._matches_pattern(value_getter(item), pattern) for pattern in patterns) + ] + if matches: + filtered[server] = matches + + return filtered + def _filter_server_tools(self, tools: list[Tool] | None, namespace: str) -> list[Tool]: """ Filter items for a Server (not namespaced) @@ -380,16 +414,15 @@ def _filter_server_tools(self, tools: list[Tool] | None, namespace: str) -> list if not tools: return [] - filters = self.config.tools or {} + filters = self.config.tools + if not filters: + return list(tools) + if namespace not in filters: - return tools + return list(tools) - patterns = filters.get(namespace, []) - return [ - tool - for tool in tools - if any(self._matches_pattern(tool.name, pattern) for pattern in patterns) - ] + filtered = self._filter_server_collections({namespace: tools}, filters, lambda tool: tool.name) + return filtered.get(namespace, []) async def _get_filtered_mcp_tools(self) -> list[Tool]: """ @@ -865,24 +898,11 @@ async def list_prompts( target = namespace if namespace is not None else server_name result = await self._aggregator.list_prompts(target) - # Apply filtering if prompts are specified in config - if self.config.prompts is not None: - filtered_result = {} - for server, prompts in result.items(): - # Check if this server has prompt filters - if server in self.config.prompts: - filtered_prompts = [] - for prompt in prompts: - # Check if prompt matches any pattern for this server - for pattern in self.config.prompts[server]: - if self._matches_pattern(prompt.name, pattern): - filtered_prompts.append(prompt) - break - if filtered_prompts: - filtered_result[server] = filtered_prompts - result = filtered_result - - return result + return self._filter_server_collections( + result, + self.config.prompts, + lambda prompt: prompt.name, + ) async def list_resources( self, namespace: str | None = None, server_name: str | None = None @@ -900,24 +920,11 @@ async def list_resources( target = namespace if namespace is not None else server_name result = await self._aggregator.list_resources(target) - # Apply filtering if resources are specified in config - if self.config.resources is not None: - filtered_result = {} - for server, resources in result.items(): - # Check if this server has resource filters - if server in self.config.resources: - filtered_resources = [] - for resource in resources: - # Check if resource matches any pattern for this server - for pattern in self.config.resources[server]: - if self._matches_pattern(resource, pattern): - filtered_resources.append(resource) - break - if filtered_resources: - filtered_result[server] = filtered_resources - result = filtered_result - - return result + return self._filter_server_collections( + result, + self.config.resources, + lambda resource: resource, + ) async def list_mcp_tools(self, namespace: str | None = None) -> Mapping[str, List[Tool]]: """ From 30808a6f688db4ce65f7ca6d3ddcd96d2563e595 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:33:41 +0000 Subject: [PATCH 7/7] break down filtering tests --- .../mcp_filtering/test_mcp_filtering.py | 179 ++++++++++-------- 1 file changed, 102 insertions(+), 77 deletions(-) diff --git a/tests/integration/mcp_filtering/test_mcp_filtering.py b/tests/integration/mcp_filtering/test_mcp_filtering.py index cf12d3edd..ebc1e8876 100644 --- a/tests/integration/mcp_filtering/test_mcp_filtering.py +++ b/tests/integration/mcp_filtering/test_mcp_filtering.py @@ -114,134 +114,159 @@ async def elapsed_agent(): @pytest.mark.integration @pytest.mark.asyncio @pytest.mark.e2e -async def test_resource_filtering_basic_agent(fast_agent): - """Test resource filtering with basic agent - no filtering vs with filtering""" +async def test_resource_filtering_returns_all_without_filters(fast_agent): + """Agents with no resource filters should expose every resource.""" fast = fast_agent - # Test 1: Agent without filtering - should have all resources @fast.agent( - name="agent_no_filter", + name="resource_no_filter_agent", instruction="Agent without resource filtering", model="passthrough", servers=["filtering_test_server"], ) - async def agent_no_filter(): + async def resource_no_filter_agent(): async with fast.run() as agent_app: - resources = await agent_app.agent_no_filter.list_resources() - resource_uris = resources["filtering_test_server"] # Already a list of URI strings - - # Should have all 4 resources - expected_resources = { + resources = await agent_app.resource_no_filter_agent.list_resources() + actual = set(resources["filtering_test_server"]) + expected = { "resource://math/constants", "resource://math/formulas", "resource://string/examples", "resource://utility/info", } - actual_resources = set(resource_uris) - assert actual_resources == expected_resources, ( - f"Expected {expected_resources}, got {actual_resources}" - ) + assert actual == expected, f"Expected {expected}, got {actual}" + + await resource_no_filter_agent() + + +@pytest.mark.integration +@pytest.mark.asyncio +@pytest.mark.e2e +async def test_resource_filtering_applies_patterns(fast_agent): + """Agents honour configured resource filters for a server.""" + fast = fast_agent - # Test 2: Agent with filtering - should have only filtered resources @fast.agent( - name="agent_with_filter", + name="resource_filtered_agent", instruction="Agent with resource filtering", model="passthrough", servers=["filtering_test_server"], resources={"filtering_test_server": ["resource://math/*", "resource://string/examples"]}, ) - async def agent_with_filter(): + async def resource_filtered_agent(): async with fast.run() as agent_app: - resources = await agent_app.agent_with_filter.list_resources() - resource_uris = resources.get( - "filtering_test_server", [] - ) # Get list or empty list if server not present - - # Should have only math resources + string examples - expected_resources = { + resources = await agent_app.resource_filtered_agent.list_resources() + actual = set(resources.get("filtering_test_server", [])) + expected = { "resource://math/constants", "resource://math/formulas", "resource://string/examples", } - actual_resources = set(resource_uris) - assert actual_resources == expected_resources, ( - f"Expected {expected_resources}, got {actual_resources}" - ) + assert actual == expected, f"Expected {expected}, got {actual}" + assert "resource://utility/info" not in actual - # Should NOT have utility resource - excluded_resources = {"resource://utility/info"} - for resource_uri in excluded_resources: - assert resource_uri not in resource_uris, ( - f"Resource {resource_uri} should have been filtered out" - ) + await resource_filtered_agent() - await agent_no_filter() - await agent_with_filter() + +@pytest.mark.integration +@pytest.mark.asyncio +@pytest.mark.e2e +async def test_resource_filtering_ignores_irrelevant_filters(fast_agent): + """Filters on other namespaces must not affect the target server.""" + fast = fast_agent + + @fast.agent( + name="resource_irrelevant_filter_agent", + instruction="Agent with irrelevant resource filters", + model="passthrough", + servers=["filtering_test_server"], + resources={"other_server": ["resource://*"]}, + ) + async def resource_irrelevant_filter_agent(): + async with fast.run() as agent_app: + resources = await agent_app.resource_irrelevant_filter_agent.list_resources() + actual = set(resources["filtering_test_server"]) + expected = { + "resource://math/constants", + "resource://math/formulas", + "resource://string/examples", + "resource://utility/info", + } + assert actual == expected, f"Expected {expected}, got {actual}" + + await resource_irrelevant_filter_agent() @pytest.mark.integration @pytest.mark.asyncio @pytest.mark.e2e -async def test_prompt_filtering_basic_agent(fast_agent): - """Test prompt filtering with basic agent - no filtering vs with filtering""" +async def test_prompt_filtering_returns_all_without_filters(fast_agent): + """Agents with no prompt filters should expose every prompt.""" fast = fast_agent - # Test 1: Agent without filtering - should have all prompts @fast.agent( - name="agent_no_filter", + name="prompt_no_filter_agent", instruction="Agent without prompt filtering", model="passthrough", servers=["filtering_test_server"], ) - async def agent_no_filter(): + async def prompt_no_filter_agent(): async with fast.run() as agent_app: - prompts = await agent_app.agent_no_filter.list_prompts() - prompt_names = [prompt.name for prompt in prompts["filtering_test_server"]] - - # Should have all 4 prompts - expected_prompts = { - "math_helper", - "math_teacher", - "string_processor", - "utility_assistant", - } - actual_prompts = set(prompt_names) - assert actual_prompts == expected_prompts, ( - f"Expected {expected_prompts}, got {actual_prompts}" - ) + prompts = await agent_app.prompt_no_filter_agent.list_prompts() + actual = {prompt.name for prompt in prompts["filtering_test_server"]} + expected = {"math_helper", "math_teacher", "string_processor", "utility_assistant"} + assert actual == expected, f"Expected {expected}, got {actual}" + + await prompt_no_filter_agent() + + +@pytest.mark.integration +@pytest.mark.asyncio +@pytest.mark.e2e +async def test_prompt_filtering_applies_patterns(fast_agent): + """Agents honour configured prompt filters for a server.""" + fast = fast_agent - # Test 2: Agent with filtering - should have only filtered prompts @fast.agent( - name="agent_with_filter", + name="prompt_filtered_agent", instruction="Agent with prompt filtering", model="passthrough", servers=["filtering_test_server"], prompts={"filtering_test_server": ["math_*", "utility_assistant"]}, ) - async def agent_with_filter(): + async def prompt_filtered_agent(): async with fast.run() as agent_app: - prompts = await agent_app.agent_with_filter.list_prompts() - prompt_list = prompts.get( - "filtering_test_server", [] - ) # Get list or empty list if server not present - prompt_names = [prompt.name for prompt in prompt_list] + prompts = await agent_app.prompt_filtered_agent.list_prompts() + actual = {prompt.name for prompt in prompts.get("filtering_test_server", [])} + expected = {"math_helper", "math_teacher", "utility_assistant"} + assert actual == expected, f"Expected {expected}, got {actual}" + assert "string_processor" not in actual - # Should have only math prompts + utility_assistant - expected_prompts = {"math_helper", "math_teacher", "utility_assistant"} - actual_prompts = set(prompt_names) - assert actual_prompts == expected_prompts, ( - f"Expected {expected_prompts}, got {actual_prompts}" - ) + await prompt_filtered_agent() - # Should NOT have string_processor - excluded_prompts = {"string_processor"} - for prompt_name in excluded_prompts: - assert prompt_name not in prompt_names, ( - f"Prompt {prompt_name} should have been filtered out" - ) - await agent_no_filter() - await agent_with_filter() +@pytest.mark.integration +@pytest.mark.asyncio +@pytest.mark.e2e +async def test_prompt_filtering_ignores_irrelevant_filters(fast_agent): + """Filters on other namespaces must not affect prompts for the target server.""" + fast = fast_agent + + @fast.agent( + name="prompt_irrelevant_filter_agent", + instruction="Agent with irrelevant prompt filters", + model="passthrough", + servers=["filtering_test_server"], + prompts={"some_other_server": ["*"]}, + ) + async def prompt_irrelevant_filter_agent(): + async with fast.run() as agent_app: + prompts = await agent_app.prompt_irrelevant_filter_agent.list_prompts() + actual = {prompt.name for prompt in prompts["filtering_test_server"]} + expected = {"math_helper", "math_teacher", "string_processor", "utility_assistant"} + assert actual == expected, f"Expected {expected}, got {actual}" + + await prompt_irrelevant_filter_agent() @pytest.mark.integration