mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Compare commits
11 commits
main
...
ep/inspect
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83cc0e6e33 |
||
|
|
2083d3f20a |
||
|
|
bf46b58e4a |
||
|
|
9a71909f33 |
||
|
|
44cd0f11cc | ||
|
|
238dbf2ddb | ||
|
|
cd2808667e | ||
|
|
9ec624cb8b | ||
|
|
43e23feb43 | ||
|
|
697ea615ee | ||
|
|
ad107d258b |
3 changed files with 465 additions and 26 deletions
|
|
@ -560,14 +560,31 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
context: MiddlewareContext[mcp.types.ListToolsRequest],
|
||||
) -> list[Tool]:
|
||||
"""
|
||||
List all available tools
|
||||
List all available tools.
|
||||
|
||||
Note: Mounted servers have already applied their own filtering via
|
||||
_list_tools_middleware(), so we should NOT re-filter them with the
|
||||
parent's tags. Only apply tag filtering to local tools.
|
||||
"""
|
||||
# Get all tools - this includes both local tools and filtered tools from mounted servers
|
||||
tools = await self._tool_manager.list_tools() # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Get the set of local tool keys (not from mounted servers)
|
||||
local_tool_keys = set(self._tool_manager._tools.keys())
|
||||
|
||||
mcp_tools: list[Tool] = []
|
||||
for tool in tools:
|
||||
if self._should_enable_component(tool):
|
||||
# Only skip filtering if:
|
||||
# 1. This server has mounted servers, AND
|
||||
# 2. The tool is from a mounted server (not in local tools)
|
||||
# For proxy servers with no mounted servers, all tools should be filtered
|
||||
if self._mounted_servers and tool.key not in local_tool_keys:
|
||||
# Tool from mounted server - already filtered, include as-is
|
||||
mcp_tools.append(tool)
|
||||
else:
|
||||
# Local tool or proxy tool - apply this server's filtering
|
||||
if self._should_enable_component(tool):
|
||||
mcp_tools.append(tool)
|
||||
|
||||
return mcp_tools
|
||||
|
||||
|
|
@ -611,14 +628,31 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
context: MiddlewareContext[dict[str, Any]],
|
||||
) -> list[Resource]:
|
||||
"""
|
||||
List all available resources
|
||||
List all available resources.
|
||||
|
||||
Note: Mounted servers have already applied their own filtering via
|
||||
_list_resources_middleware(), so we should NOT re-filter them with the
|
||||
parent's tags. Only apply tag filtering to local resources.
|
||||
"""
|
||||
# Get all resources - this includes both local resources and filtered resources from mounted servers
|
||||
resources = await self._resource_manager.list_resources() # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Get the set of local resource keys (not from mounted servers)
|
||||
local_resource_keys = set(self._resource_manager._resources.keys())
|
||||
|
||||
mcp_resources: list[Resource] = []
|
||||
for resource in resources:
|
||||
if self._should_enable_component(resource):
|
||||
# Only skip filtering if:
|
||||
# 1. This server has mounted servers, AND
|
||||
# 2. The resource is from a mounted server (not in local resources)
|
||||
# For proxy servers with no mounted servers, all resources should be filtered
|
||||
if self._mounted_servers and resource.key not in local_resource_keys:
|
||||
# Resource from mounted server - already filtered, include as-is
|
||||
mcp_resources.append(resource)
|
||||
else:
|
||||
# Local resource or proxy resource - apply this server's filtering
|
||||
if self._should_enable_component(resource):
|
||||
mcp_resources.append(resource)
|
||||
|
||||
return mcp_resources
|
||||
|
||||
|
|
@ -665,14 +699,31 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
context: MiddlewareContext[dict[str, Any]],
|
||||
) -> list[ResourceTemplate]:
|
||||
"""
|
||||
List all available resource templates
|
||||
List all available resource templates.
|
||||
|
||||
Note: Mounted servers have already applied their own filtering via
|
||||
_list_resource_templates_middleware(), so we should NOT re-filter them with the
|
||||
parent's tags. Only apply tag filtering to local templates.
|
||||
"""
|
||||
# Get all templates - this includes both local templates and filtered templates from mounted servers
|
||||
templates = await self._resource_manager.list_resource_templates() # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Get the set of local template keys (not from mounted servers)
|
||||
local_template_keys = set(self._resource_manager._templates.keys())
|
||||
|
||||
mcp_templates: list[ResourceTemplate] = []
|
||||
for template in templates:
|
||||
if self._should_enable_component(template):
|
||||
# Only skip filtering if:
|
||||
# 1. This server has mounted servers, AND
|
||||
# 2. The template is from a mounted server (not in local templates)
|
||||
# For proxy servers with no mounted servers, all templates should be filtered
|
||||
if self._mounted_servers and template.key not in local_template_keys:
|
||||
# Template from mounted server - already filtered, include as-is
|
||||
mcp_templates.append(template)
|
||||
else:
|
||||
# Local template or proxy template - apply this server's filtering
|
||||
if self._should_enable_component(template):
|
||||
mcp_templates.append(template)
|
||||
|
||||
return mcp_templates
|
||||
|
||||
|
|
@ -717,14 +768,31 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
context: MiddlewareContext[mcp.types.ListPromptsRequest],
|
||||
) -> list[Prompt]:
|
||||
"""
|
||||
List all available prompts
|
||||
List all available prompts.
|
||||
|
||||
Note: Mounted servers have already applied their own filtering via
|
||||
_list_prompts_middleware(), so we should NOT re-filter them with the
|
||||
parent's tags. Only apply tag filtering to local prompts.
|
||||
"""
|
||||
# Get all prompts - this includes both local prompts and filtered prompts from mounted servers
|
||||
prompts = await self._prompt_manager.list_prompts() # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Get the set of local prompt keys (not from mounted servers)
|
||||
local_prompt_keys = set(self._prompt_manager._prompts.keys())
|
||||
|
||||
mcp_prompts: list[Prompt] = []
|
||||
for prompt in prompts:
|
||||
if self._should_enable_component(prompt):
|
||||
# Only skip filtering if:
|
||||
# 1. This server has mounted servers, AND
|
||||
# 2. The prompt is from a mounted server (not in local prompts)
|
||||
# For proxy servers with no mounted servers, all prompts should be filtered
|
||||
if self._mounted_servers and prompt.key not in local_prompt_keys:
|
||||
# Prompt from mounted server - already filtered, include as-is
|
||||
mcp_prompts.append(prompt)
|
||||
else:
|
||||
# Local prompt or proxy prompt - apply this server's filtering
|
||||
if self._should_enable_component(prompt):
|
||||
mcp_prompts.append(prompt)
|
||||
|
||||
return mcp_prompts
|
||||
|
||||
|
|
|
|||
|
|
@ -104,21 +104,21 @@ 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
|
||||
filtered_tools = await mcp._list_tools_middleware()
|
||||
filtered_prompts = await mcp._list_prompts_middleware()
|
||||
filtered_resources = await mcp._list_resources_middleware()
|
||||
filtered_templates = await mcp._list_resource_templates_middleware()
|
||||
|
||||
# Extract detailed tool information
|
||||
tool_infos = []
|
||||
for key, tool in tools_dict.items():
|
||||
for tool in filtered_tools:
|
||||
# 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 +132,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 filtered_prompts:
|
||||
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 +150,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 filtered_resources:
|
||||
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 +170,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 filtered_templates:
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -412,6 +412,377 @@ 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"
|
||||
|
||||
async def test_inspect_mounted_servers_with_tag_filtering(self):
|
||||
"""Test that inspect respects tag filtering in mounted servers."""
|
||||
# Create child servers with different tag filtering rules
|
||||
child1 = FastMCP("child1", include_tags={"admin"})
|
||||
|
||||
@child1.tool(tags={"admin"})
|
||||
def child1_admin_tool() -> str:
|
||||
"""Child1 Admin Tool."""
|
||||
return "admin"
|
||||
|
||||
@child1.tool(tags={"public"})
|
||||
def child1_public_tool() -> str:
|
||||
"""Child1 Public Tool (should be hidden)."""
|
||||
return "public"
|
||||
|
||||
child2 = FastMCP("child2", exclude_tags={"internal"})
|
||||
|
||||
@child2.tool(tags={"public"})
|
||||
def child2_public_tool() -> str:
|
||||
"""Child2 Public Tool."""
|
||||
return "public"
|
||||
|
||||
@child2.tool(tags={"internal"})
|
||||
def child2_internal_tool() -> str:
|
||||
"""Child2 Internal Tool (should be hidden)."""
|
||||
return "internal"
|
||||
|
||||
# Create parent server with its own filtering
|
||||
parent = FastMCP("parent", include_tags={"show"})
|
||||
|
||||
@parent.tool(tags={"show"})
|
||||
def parent_show_tool() -> str:
|
||||
"""Parent Show Tool."""
|
||||
return "show"
|
||||
|
||||
@parent.tool()
|
||||
def parent_hide_tool() -> str:
|
||||
"""Parent Hide Tool (should be hidden)."""
|
||||
return "hide"
|
||||
|
||||
# Mount children
|
||||
parent.mount(child1, prefix="c1")
|
||||
parent.mount(child2, prefix="c2")
|
||||
|
||||
# Inspect the parent server
|
||||
info = await inspect_fastmcp(parent)
|
||||
|
||||
# Verify parent's filtering is applied to local tools only
|
||||
parent_tool_keys = [
|
||||
t.key
|
||||
for t in info.tools
|
||||
if not t.key.startswith("c1_") and not t.key.startswith("c2_")
|
||||
]
|
||||
assert "parent_show_tool" in parent_tool_keys
|
||||
assert "parent_hide_tool" not in parent_tool_keys
|
||||
|
||||
# Verify child1's filtering is preserved (only admin tools)
|
||||
child1_tool_keys = [t.key for t in info.tools if t.key.startswith("c1_")]
|
||||
assert "c1_child1_admin_tool" in child1_tool_keys
|
||||
assert "c1_child1_public_tool" not in child1_tool_keys
|
||||
|
||||
# Verify child2's filtering is preserved (exclude internal tools)
|
||||
child2_tool_keys = [t.key for t in info.tools if t.key.startswith("c2_")]
|
||||
assert "c2_child2_public_tool" in child2_tool_keys
|
||||
assert "c2_child2_internal_tool" not in child2_tool_keys
|
||||
|
||||
async def test_inspect_mounted_servers_with_resources_filtering(self):
|
||||
"""Test that inspect respects tag filtering for resources in mounted servers."""
|
||||
# Create child server with resource filtering
|
||||
child = FastMCP("child", include_tags={"public"})
|
||||
|
||||
@child.resource("resource://child/public", tags={"public"})
|
||||
def child_public_resource() -> str:
|
||||
"""Child Public Resource."""
|
||||
return "public"
|
||||
|
||||
@child.resource("resource://child/private", tags={"private"})
|
||||
def child_private_resource() -> str:
|
||||
"""Child Private Resource (should be hidden)."""
|
||||
return "private"
|
||||
|
||||
# Create parent server with its own filtering
|
||||
parent = FastMCP("parent", include_tags={"show"})
|
||||
|
||||
@parent.resource("resource://parent/show", tags={"show"})
|
||||
def parent_show_resource() -> str:
|
||||
"""Parent Show Resource."""
|
||||
return "show"
|
||||
|
||||
@parent.resource("resource://parent/hide")
|
||||
def parent_hide_resource() -> str:
|
||||
"""Parent Hide Resource (should be hidden)."""
|
||||
return "hide"
|
||||
|
||||
# Mount child
|
||||
parent.mount(child, prefix="c")
|
||||
|
||||
# Inspect the parent server
|
||||
info = await inspect_fastmcp(parent)
|
||||
|
||||
# Verify parent's resources
|
||||
parent_resource_uris = [
|
||||
r.uri for r in info.resources if not r.uri.startswith("resource://c/")
|
||||
]
|
||||
assert "resource://parent/show" in parent_resource_uris
|
||||
assert "resource://parent/hide" not in parent_resource_uris
|
||||
|
||||
# Verify child's filtering is preserved
|
||||
child_resource_uris = [
|
||||
r.uri for r in info.resources if r.uri.startswith("resource://c/")
|
||||
]
|
||||
assert "resource://c/child/public" in child_resource_uris
|
||||
assert "resource://c/child/private" not in child_resource_uris
|
||||
|
||||
async def test_inspect_mounted_servers_with_prompts_filtering(self):
|
||||
"""Test that inspect respects tag filtering for prompts in mounted servers."""
|
||||
# Create child server with prompt filtering
|
||||
child = FastMCP("child", exclude_tags={"internal"})
|
||||
|
||||
@child.prompt(tags={"user"})
|
||||
def child_user_prompt() -> list:
|
||||
"""Child User Prompt."""
|
||||
return [{"role": "user", "content": "user"}]
|
||||
|
||||
@child.prompt(tags={"internal"})
|
||||
def child_internal_prompt() -> list:
|
||||
"""Child Internal Prompt (should be hidden)."""
|
||||
return [{"role": "user", "content": "internal"}]
|
||||
|
||||
# Create parent server with its own filtering
|
||||
parent = FastMCP("parent", include_tags={"api"})
|
||||
|
||||
@parent.prompt(tags={"api"})
|
||||
def parent_api_prompt() -> list:
|
||||
"""Parent API Prompt."""
|
||||
return [{"role": "user", "content": "api"}]
|
||||
|
||||
@parent.prompt()
|
||||
def parent_other_prompt() -> list:
|
||||
"""Parent Other Prompt (should be hidden)."""
|
||||
return [{"role": "user", "content": "other"}]
|
||||
|
||||
# Mount child
|
||||
parent.mount(child, prefix="c")
|
||||
|
||||
# Inspect the parent server
|
||||
info = await inspect_fastmcp(parent)
|
||||
|
||||
# Verify parent's prompts
|
||||
parent_prompt_keys = [p.key for p in info.prompts if not p.key.startswith("c_")]
|
||||
assert "parent_api_prompt" in parent_prompt_keys
|
||||
assert "parent_other_prompt" not in parent_prompt_keys
|
||||
|
||||
# Verify child's filtering is preserved
|
||||
child_prompt_keys = [p.key for p in info.prompts if p.key.startswith("c_")]
|
||||
assert "c_child_user_prompt" in child_prompt_keys
|
||||
assert "c_child_internal_prompt" not in child_prompt_keys
|
||||
|
||||
|
||||
class TestFormatFunctions:
|
||||
"""Tests for the formatting functions."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue