fix: fastmcp inspect should respect tags

This commit is contained in:
Edward Park 2025-10-09 16:38:33 -07:00
commit ad107d258b
2 changed files with 245 additions and 18 deletions

View file

@ -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,

View file

@ -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."""