diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py index f3303a8f4..7b24b61ae 100644 --- a/src/fastmcp/utilities/inspect.py +++ b/src/fastmcp/utilities/inspect.py @@ -104,21 +104,35 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: Returns: FastMCPInfo dataclass containing the extracted information """ - # Get all the components using FastMCP2's direct methods - tools_dict = await mcp.get_tools() - prompts_dict = await mcp.get_prompts() - resources_dict = await mcp.get_resources() - templates_dict = await mcp.get_resource_templates() + # Get all components and apply server-level filtering + all_tools = await mcp._tool_manager.list_tools() + all_prompts = await mcp._prompt_manager.list_prompts() + all_resources = await mcp._resource_manager.list_resources() + all_templates = await mcp._resource_manager.list_resource_templates() + + # Filter components based on server's include_tags/exclude_tags + tools_list = [tool for tool in all_tools if mcp._should_enable_component(tool)] + prompts_list = [ + prompt for prompt in all_prompts if mcp._should_enable_component(prompt) + ] + resources_list = [ + resource for resource in all_resources if mcp._should_enable_component(resource) + ] + templates_list = [ + template + for template in all_templates + if mcp._should_enable_component(template) + ] # Extract detailed tool information tool_infos = [] - for key, tool in tools_dict.items(): + for tool in tools_list: # Convert to MCP tool to get input schema - mcp_tool = tool.to_mcp_tool(name=key) + mcp_tool = tool.to_mcp_tool(name=tool.key) tool_infos.append( ToolInfo( - key=key, - name=tool.name or key, + key=tool.key, + name=tool.name or tool.key, description=tool.description, input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {}, output_schema=tool.output_schema, @@ -132,11 +146,11 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: # Extract detailed prompt information prompt_infos = [] - for key, prompt in prompts_dict.items(): + for prompt in prompts_list: prompt_infos.append( PromptInfo( - key=key, - name=prompt.name or key, + key=prompt.key, + name=prompt.name or prompt.key, description=prompt.description, arguments=[arg.model_dump() for arg in prompt.arguments] if prompt.arguments @@ -150,11 +164,11 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: # Extract detailed resource information resource_infos = [] - for key, resource in resources_dict.items(): + for resource in resources_list: resource_infos.append( ResourceInfo( - key=key, - uri=key, # For v2, key is the URI + key=resource.key, + uri=resource.key, # For v2, key is the URI name=resource.name, description=resource.description, mime_type=resource.mime_type, @@ -170,11 +184,11 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo: # Extract detailed template information template_infos = [] - for key, template in templates_dict.items(): + for template in templates_list: template_infos.append( TemplateInfo( - key=key, - uri_template=key, # For v2, key is the URI template + key=template.key, + uri_template=template.key, # For v2, key is the URI template name=template.name, description=template.description, mime_type=template.mime_type, diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py index 420335c82..0c0155889 100644 --- a/tests/utilities/test_inspect.py +++ b/tests/utilities/test_inspect.py @@ -412,6 +412,219 @@ class TestFastMCP1xCompatibility: assert len(info2x.templates) == 0 +class TestInspectWithTagFiltering: + """Tests for inspect functionality with include_tags and exclude_tags.""" + + async def test_inspect_with_include_tags_filters_tools(self): + """Test that inspect respects include_tags for tools.""" + mcp = FastMCP("TaggedServer", include_tags={"api"}) + + @mcp.tool(tags={"api"}) + def api_tool() -> str: + """API tool.""" + return "api" + + @mcp.tool(tags={"internal"}) + def internal_tool() -> str: + """Internal tool.""" + return "internal" + + @mcp.tool(tags={"api", "internal"}) + def mixed_tool() -> str: + """Mixed tool.""" + return "mixed" + + info = await inspect_fastmcp(mcp) + + # Only tools with 'api' tag should be included + assert len(info.tools) == 2 + tool_names = [tool.name for tool in info.tools] + assert "api_tool" in tool_names + assert "mixed_tool" in tool_names + assert "internal_tool" not in tool_names + + async def test_inspect_with_exclude_tags_filters_tools(self): + """Test that inspect respects exclude_tags for tools.""" + mcp = FastMCP("TaggedServer", exclude_tags={"internal"}) + + @mcp.tool(tags={"api"}) + def api_tool() -> str: + """API tool.""" + return "api" + + @mcp.tool(tags={"internal"}) + def internal_tool() -> str: + """Internal tool.""" + return "internal" + + @mcp.tool(tags={"api", "internal"}) + def mixed_tool() -> str: + """Mixed tool.""" + return "mixed" + + info = await inspect_fastmcp(mcp) + + # Only tools without 'internal' tag should be included + assert len(info.tools) == 1 + tool_names = [tool.name for tool in info.tools] + assert "api_tool" in tool_names + assert "internal_tool" not in tool_names + assert "mixed_tool" not in tool_names + + async def test_inspect_with_include_tags_filters_prompts(self): + """Test that inspect respects include_tags for prompts.""" + mcp = FastMCP("TaggedServer", include_tags={"user-facing"}) + + @mcp.prompt(tags={"user-facing"}) + def user_prompt(text: str) -> list: + """User prompt.""" + return [{"role": "user", "content": text}] + + @mcp.prompt(tags={"admin"}) + def admin_prompt(text: str) -> list: + """Admin prompt.""" + return [{"role": "user", "content": text}] + + info = await inspect_fastmcp(mcp) + + # Only prompts with 'user-facing' tag should be included + assert len(info.prompts) == 1 + prompt_names = [prompt.name for prompt in info.prompts] + assert "user_prompt" in prompt_names + assert "admin_prompt" not in prompt_names + + async def test_inspect_with_include_tags_filters_resources(self): + """Test that inspect respects include_tags for resources.""" + mcp = FastMCP("TaggedServer", include_tags={"public"}) + + @mcp.resource("resource://public", tags={"public"}) + def public_resource() -> str: + """Public resource.""" + return "public data" + + @mcp.resource("resource://private", tags={"private"}) + def private_resource() -> str: + """Private resource.""" + return "private data" + + info = await inspect_fastmcp(mcp) + + # Only resources with 'public' tag should be included + assert len(info.resources) == 1 + resource_uris = [res.uri for res in info.resources] + assert "resource://public" in resource_uris + assert "resource://private" not in resource_uris + + async def test_inspect_with_include_tags_filters_templates(self): + """Test that inspect respects include_tags for resource templates.""" + mcp = FastMCP("TaggedServer", include_tags={"public"}) + + @mcp.resource("resource://public/{id}", tags={"public"}) + def public_template(id: str) -> str: + """Public template.""" + return f"public {id}" + + @mcp.resource("resource://private/{id}", tags={"private"}) + def private_template(id: str) -> str: + """Private template.""" + return f"private {id}" + + info = await inspect_fastmcp(mcp) + + # Only templates with 'public' tag should be included + assert len(info.templates) == 1 + template_uris = [tmpl.uri_template for tmpl in info.templates] + assert "resource://public/{id}" in template_uris + assert "resource://private/{id}" not in template_uris + + async def test_inspect_with_multiple_component_types_filtered(self): + """Test that inspect filters all component types consistently.""" + mcp = FastMCP("TaggedServer", include_tags={"api"}) + + @mcp.tool(tags={"api"}) + def api_tool() -> str: + return "api" + + @mcp.tool(tags={"internal"}) + def internal_tool() -> str: + return "internal" + + @mcp.prompt(tags={"api"}) + def api_prompt() -> list: + return [{"role": "user", "content": "api"}] + + @mcp.prompt(tags={"internal"}) + def internal_prompt() -> list: + return [{"role": "user", "content": "internal"}] + + @mcp.resource("resource://api", tags={"api"}) + def api_resource() -> str: + return "api" + + @mcp.resource("resource://internal", tags={"internal"}) + def internal_resource() -> str: + return "internal" + + info = await inspect_fastmcp(mcp) + + # All component types should be filtered + assert len(info.tools) == 1 + assert len(info.prompts) == 1 + assert len(info.resources) == 1 + assert info.tools[0].name == "api_tool" + assert info.prompts[0].name == "api_prompt" + assert info.resources[0].uri == "resource://api" + + async def test_format_mcp_respects_filtering(self): + """Test that format_mcp_info also respects tag filtering.""" + mcp = FastMCP("TaggedServer", include_tags={"api"}) + + @mcp.tool(tags={"api"}) + def api_tool() -> str: + """API tool.""" + return "api" + + @mcp.tool(tags={"internal"}) + def internal_tool() -> str: + """Internal tool.""" + return "internal" + + json_bytes = await format_mcp_info(mcp) + + import json + + data = json.loads(json_bytes) + + # Only tools with 'api' tag should be in MCP format + assert len(data["tools"]) == 1 + assert data["tools"][0]["name"] == "api_tool" + + async def test_format_fastmcp_respects_filtering(self): + """Test that format_fastmcp_info also respects tag filtering.""" + mcp = FastMCP("TaggedServer", include_tags={"api"}) + + @mcp.tool(tags={"api"}) + def api_tool() -> str: + """API tool.""" + return "api" + + @mcp.tool(tags={"internal"}) + def internal_tool() -> str: + """Internal tool.""" + return "internal" + + info = await inspect_fastmcp(mcp) + json_bytes = await format_fastmcp_info(info) + + import json + + data = json.loads(json_bytes) + + # Only tools with 'api' tag should be in FastMCP format + assert len(data["tools"]) == 1 + assert data["tools"][0]["name"] == "api_tool" + + class TestFormatFunctions: """Tests for the formatting functions."""