Spaces:
Running
Running
Merge pull request #748 from jlowin/include-exclude
Browse files- src/fastmcp/server/server.py +69 -24
- src/fastmcp/settings.py +30 -0
- src/fastmcp/tools/tool.py +1 -3
- tests/resources/test_resource_template.py +41 -0
- tests/server/test_server.py +103 -0
- tests/server/test_server_interactions.py +277 -0
src/fastmcp/server/server.py
CHANGED
|
@@ -59,6 +59,7 @@ from fastmcp.settings import Settings
|
|
| 59 |
from fastmcp.tools import ToolManager
|
| 60 |
from fastmcp.tools.tool import FunctionTool, Tool
|
| 61 |
from fastmcp.utilities.cache import TimedCache
|
|
|
|
| 62 |
from fastmcp.utilities.logging import get_logger
|
| 63 |
from fastmcp.utilities.mcp_config import MCPConfig
|
| 64 |
|
|
@@ -130,6 +131,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 130 |
mask_error_details: bool | None = None,
|
| 131 |
tools: list[Tool | Callable[..., Any]] | None = None,
|
| 132 |
dependencies: list[str] | None = None,
|
|
|
|
|
|
|
| 133 |
# ---
|
| 134 |
# ---
|
| 135 |
# --- The following arguments are DEPRECATED ---
|
|
@@ -191,6 +194,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 191 |
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
| 192 |
self.add_tool(tool)
|
| 193 |
|
|
|
|
|
|
|
|
|
|
| 194 |
# Set up MCP protocol handlers
|
| 195 |
self._setup_handlers()
|
| 196 |
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
|
@@ -295,12 +301,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 295 |
def _setup_handlers(self) -> None:
|
| 296 |
"""Set up core MCP protocol handlers."""
|
| 297 |
self._mcp_server.list_tools()(self._mcp_list_tools)
|
| 298 |
-
self._mcp_server.call_tool()(self._mcp_call_tool)
|
| 299 |
self._mcp_server.list_resources()(self._mcp_list_resources)
|
| 300 |
-
self._mcp_server.
|
| 301 |
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
|
|
|
|
|
|
| 302 |
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
| 303 |
-
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
| 304 |
|
| 305 |
async def get_tools(self) -> dict[str, Tool]:
|
| 306 |
"""Get all registered tools, indexed by registered key."""
|
|
@@ -450,9 +456,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 450 |
|
| 451 |
"""
|
| 452 |
tools = await self.get_tools()
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
async def _mcp_list_resources(self) -> list[MCPResource]:
|
| 458 |
"""
|
|
@@ -461,11 +471,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 461 |
|
| 462 |
"""
|
| 463 |
resources = await self.get_resources()
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
|
| 470 |
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
| 471 |
"""
|
|
@@ -474,11 +484,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 474 |
|
| 475 |
"""
|
| 476 |
templates = await self.get_resource_templates()
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
|
| 483 |
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
| 484 |
"""
|
|
@@ -487,11 +497,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 487 |
|
| 488 |
"""
|
| 489 |
prompts = await self.get_prompts()
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
|
| 496 |
async def _mcp_call_tool(
|
| 497 |
self, key: str, arguments: dict[str, Any]
|
|
@@ -539,7 +549,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 539 |
# Get tool, checking first from our tools, then from the mounted servers
|
| 540 |
if self._tool_manager.has_tool(key):
|
| 541 |
tool = self._tool_manager.get_tool(key)
|
| 542 |
-
if not
|
| 543 |
raise DisabledError(f"Tool {key!r} is disabled")
|
| 544 |
return await self._tool_manager.call_tool(key, arguments)
|
| 545 |
|
|
@@ -576,7 +586,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 576 |
"""
|
| 577 |
if self._resource_manager.has_resource(uri):
|
| 578 |
resource = await self._resource_manager.get_resource(uri)
|
| 579 |
-
if not
|
| 580 |
raise DisabledError(f"Resource {str(uri)!r} is disabled")
|
| 581 |
content = await self._resource_manager.read_resource(uri)
|
| 582 |
return [
|
|
@@ -630,7 +640,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 630 |
# Get prompt, checking first from our prompts, then from the mounted servers
|
| 631 |
if self._prompt_manager.has_prompt(name):
|
| 632 |
prompt = self._prompt_manager.get_prompt(name)
|
| 633 |
-
if not
|
| 634 |
raise DisabledError(f"Prompt {name!r} is disabled")
|
| 635 |
return await self._prompt_manager.render_prompt(name, arguments)
|
| 636 |
|
|
@@ -1654,6 +1664,41 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1654 |
|
| 1655 |
return cls.as_proxy(client, **settings)
|
| 1656 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1657 |
|
| 1658 |
class MountedServer:
|
| 1659 |
def __init__(
|
|
|
|
| 59 |
from fastmcp.tools import ToolManager
|
| 60 |
from fastmcp.tools.tool import FunctionTool, Tool
|
| 61 |
from fastmcp.utilities.cache import TimedCache
|
| 62 |
+
from fastmcp.utilities.components import FastMCPComponent
|
| 63 |
from fastmcp.utilities.logging import get_logger
|
| 64 |
from fastmcp.utilities.mcp_config import MCPConfig
|
| 65 |
|
|
|
|
| 131 |
mask_error_details: bool | None = None,
|
| 132 |
tools: list[Tool | Callable[..., Any]] | None = None,
|
| 133 |
dependencies: list[str] | None = None,
|
| 134 |
+
include_tags: set[str] | None = None,
|
| 135 |
+
exclude_tags: set[str] | None = None,
|
| 136 |
# ---
|
| 137 |
# ---
|
| 138 |
# --- The following arguments are DEPRECATED ---
|
|
|
|
| 194 |
tool = Tool.from_function(tool, serializer=self._tool_serializer)
|
| 195 |
self.add_tool(tool)
|
| 196 |
|
| 197 |
+
self.include_tags = include_tags
|
| 198 |
+
self.exclude_tags = exclude_tags
|
| 199 |
+
|
| 200 |
# Set up MCP protocol handlers
|
| 201 |
self._setup_handlers()
|
| 202 |
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
|
|
|
| 301 |
def _setup_handlers(self) -> None:
|
| 302 |
"""Set up core MCP protocol handlers."""
|
| 303 |
self._mcp_server.list_tools()(self._mcp_list_tools)
|
|
|
|
| 304 |
self._mcp_server.list_resources()(self._mcp_list_resources)
|
| 305 |
+
self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
|
| 306 |
self._mcp_server.list_prompts()(self._mcp_list_prompts)
|
| 307 |
+
self._mcp_server.call_tool()(self._mcp_call_tool)
|
| 308 |
+
self._mcp_server.read_resource()(self._mcp_read_resource)
|
| 309 |
self._mcp_server.get_prompt()(self._mcp_get_prompt)
|
|
|
|
| 310 |
|
| 311 |
async def get_tools(self) -> dict[str, Tool]:
|
| 312 |
"""Get all registered tools, indexed by registered key."""
|
|
|
|
| 456 |
|
| 457 |
"""
|
| 458 |
tools = await self.get_tools()
|
| 459 |
+
|
| 460 |
+
mcp_tools: list[MCPTool] = []
|
| 461 |
+
for key, tool in tools.items():
|
| 462 |
+
if self._should_enable_component(tool):
|
| 463 |
+
mcp_tools.append(tool.to_mcp_tool(name=key))
|
| 464 |
+
|
| 465 |
+
return mcp_tools
|
| 466 |
|
| 467 |
async def _mcp_list_resources(self) -> list[MCPResource]:
|
| 468 |
"""
|
|
|
|
| 471 |
|
| 472 |
"""
|
| 473 |
resources = await self.get_resources()
|
| 474 |
+
mcp_resources: list[MCPResource] = []
|
| 475 |
+
for key, resource in resources.items():
|
| 476 |
+
if self._should_enable_component(resource):
|
| 477 |
+
mcp_resources.append(resource.to_mcp_resource(uri=key))
|
| 478 |
+
return mcp_resources
|
| 479 |
|
| 480 |
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
|
| 481 |
"""
|
|
|
|
| 484 |
|
| 485 |
"""
|
| 486 |
templates = await self.get_resource_templates()
|
| 487 |
+
mcp_templates: list[MCPResourceTemplate] = []
|
| 488 |
+
for key, template in templates.items():
|
| 489 |
+
if self._should_enable_component(template):
|
| 490 |
+
mcp_templates.append(template.to_mcp_template(uriTemplate=key))
|
| 491 |
+
return mcp_templates
|
| 492 |
|
| 493 |
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
|
| 494 |
"""
|
|
|
|
| 497 |
|
| 498 |
"""
|
| 499 |
prompts = await self.get_prompts()
|
| 500 |
+
mcp_prompts: list[MCPPrompt] = []
|
| 501 |
+
for key, prompt in prompts.items():
|
| 502 |
+
if self._should_enable_component(prompt):
|
| 503 |
+
mcp_prompts.append(prompt.to_mcp_prompt(name=key))
|
| 504 |
+
return mcp_prompts
|
| 505 |
|
| 506 |
async def _mcp_call_tool(
|
| 507 |
self, key: str, arguments: dict[str, Any]
|
|
|
|
| 549 |
# Get tool, checking first from our tools, then from the mounted servers
|
| 550 |
if self._tool_manager.has_tool(key):
|
| 551 |
tool = self._tool_manager.get_tool(key)
|
| 552 |
+
if not self._should_enable_component(tool):
|
| 553 |
raise DisabledError(f"Tool {key!r} is disabled")
|
| 554 |
return await self._tool_manager.call_tool(key, arguments)
|
| 555 |
|
|
|
|
| 586 |
"""
|
| 587 |
if self._resource_manager.has_resource(uri):
|
| 588 |
resource = await self._resource_manager.get_resource(uri)
|
| 589 |
+
if not self._should_enable_component(resource):
|
| 590 |
raise DisabledError(f"Resource {str(uri)!r} is disabled")
|
| 591 |
content = await self._resource_manager.read_resource(uri)
|
| 592 |
return [
|
|
|
|
| 640 |
# Get prompt, checking first from our prompts, then from the mounted servers
|
| 641 |
if self._prompt_manager.has_prompt(name):
|
| 642 |
prompt = self._prompt_manager.get_prompt(name)
|
| 643 |
+
if not self._should_enable_component(prompt):
|
| 644 |
raise DisabledError(f"Prompt {name!r} is disabled")
|
| 645 |
return await self._prompt_manager.render_prompt(name, arguments)
|
| 646 |
|
|
|
|
| 1664 |
|
| 1665 |
return cls.as_proxy(client, **settings)
|
| 1666 |
|
| 1667 |
+
def _should_enable_component(
|
| 1668 |
+
self,
|
| 1669 |
+
component: FastMCPComponent,
|
| 1670 |
+
) -> bool:
|
| 1671 |
+
"""
|
| 1672 |
+
Given a component, determine if it should be enabled. Returns True if it should be enabled; False if it should not.
|
| 1673 |
+
|
| 1674 |
+
Rules:
|
| 1675 |
+
• If the component's enabled property is False, always return False.
|
| 1676 |
+
• If both include_tags and exclude_tags are None, return True.
|
| 1677 |
+
• If exclude_tags is provided, check each exclude tag:
|
| 1678 |
+
- If the exclude tag is a string, it must be present in the input tags to exclude.
|
| 1679 |
+
• If include_tags is provided, check each include tag:
|
| 1680 |
+
- If the include tag is a string, it must be present in the input tags to include.
|
| 1681 |
+
• If include_tags is provided and none of the include tags match, return False.
|
| 1682 |
+
• If include_tags is not provided, return True.
|
| 1683 |
+
"""
|
| 1684 |
+
if not component.enabled:
|
| 1685 |
+
return False
|
| 1686 |
+
|
| 1687 |
+
if self.include_tags is None and self.exclude_tags is None:
|
| 1688 |
+
return True
|
| 1689 |
+
|
| 1690 |
+
if self.exclude_tags is not None:
|
| 1691 |
+
if any(etag in component.tags for etag in self.exclude_tags):
|
| 1692 |
+
return False
|
| 1693 |
+
|
| 1694 |
+
if self.include_tags is not None:
|
| 1695 |
+
if any(itag in component.tags for itag in self.include_tags):
|
| 1696 |
+
return True
|
| 1697 |
+
else:
|
| 1698 |
+
return False
|
| 1699 |
+
|
| 1700 |
+
return True
|
| 1701 |
+
|
| 1702 |
|
| 1703 |
class MountedServer:
|
| 1704 |
def __init__(
|
src/fastmcp/settings.py
CHANGED
|
@@ -233,3 +233,33 @@ class Settings(BaseSettings):
|
|
| 233 |
),
|
| 234 |
),
|
| 235 |
] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
),
|
| 234 |
),
|
| 235 |
] = None
|
| 236 |
+
|
| 237 |
+
include_tags: Annotated[
|
| 238 |
+
set[str] | None,
|
| 239 |
+
Field(
|
| 240 |
+
default=None,
|
| 241 |
+
description=inspect.cleandoc(
|
| 242 |
+
"""
|
| 243 |
+
If provided, only components that match these tags will be
|
| 244 |
+
exposed to clients. A component is considered to match if ANY of
|
| 245 |
+
its tags match ANY of the tags in the set.
|
| 246 |
+
"""
|
| 247 |
+
),
|
| 248 |
+
),
|
| 249 |
+
] = None
|
| 250 |
+
exclude_tags: Annotated[
|
| 251 |
+
set[str] | None,
|
| 252 |
+
Field(
|
| 253 |
+
default=None,
|
| 254 |
+
description=inspect.cleandoc(
|
| 255 |
+
"""
|
| 256 |
+
If provided, components that match these tags will be excluded
|
| 257 |
+
from the server. A component is considered to match if ANY of
|
| 258 |
+
its tags match ANY of the tags in the set.
|
| 259 |
+
"""
|
| 260 |
+
),
|
| 261 |
+
),
|
| 262 |
+
] = None
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
settings = Settings()
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
import json
|
| 5 |
-
from abc import ABC, abstractmethod
|
| 6 |
from collections.abc import Callable
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from typing import TYPE_CHECKING, Any
|
|
@@ -33,7 +32,7 @@ def default_serializer(data: Any) -> str:
|
|
| 33 |
return pydantic_core.to_json(data, fallback=str, indent=2).decode()
|
| 34 |
|
| 35 |
|
| 36 |
-
class Tool(FastMCPComponent
|
| 37 |
"""Internal tool registration info."""
|
| 38 |
|
| 39 |
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
|
@@ -76,7 +75,6 @@ class Tool(FastMCPComponent, ABC):
|
|
| 76 |
enabled=enabled,
|
| 77 |
)
|
| 78 |
|
| 79 |
-
@abstractmethod
|
| 80 |
async def run(
|
| 81 |
self, arguments: dict[str, Any]
|
| 82 |
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
|
|
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
import json
|
|
|
|
| 5 |
from collections.abc import Callable
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from typing import TYPE_CHECKING, Any
|
|
|
|
| 32 |
return pydantic_core.to_json(data, fallback=str, indent=2).decode()
|
| 33 |
|
| 34 |
|
| 35 |
+
class Tool(FastMCPComponent):
|
| 36 |
"""Internal tool registration info."""
|
| 37 |
|
| 38 |
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
|
|
|
|
| 75 |
enabled=enabled,
|
| 76 |
)
|
| 77 |
|
|
|
|
| 78 |
async def run(
|
| 79 |
self, arguments: dict[str, Any]
|
| 80 |
) -> list[TextContent | ImageContent | EmbeddedResource]:
|
tests/resources/test_resource_template.py
CHANGED
|
@@ -558,6 +558,47 @@ class TestMatchUriTemplate:
|
|
| 558 |
result = match_uri_template(uri=uri, uri_template=uri_template)
|
| 559 |
assert result == expected_params
|
| 560 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
|
| 562 |
class TestContextHandling:
|
| 563 |
"""Test context handling in resource templates."""
|
|
|
|
| 558 |
result = match_uri_template(uri=uri, uri_template=uri_template)
|
| 559 |
assert result == expected_params
|
| 560 |
|
| 561 |
+
@pytest.mark.parametrize(
|
| 562 |
+
"uri, expected_params",
|
| 563 |
+
[
|
| 564 |
+
("resource://test_foo", {"x": "foo"}),
|
| 565 |
+
("resource://test_bar", {"x": "bar"}),
|
| 566 |
+
("resource://test_hello", {"x": "hello"}),
|
| 567 |
+
("resource://test_with_underscores", {"x": "with_underscores"}),
|
| 568 |
+
("resource://test_", None), # Empty parameter not matched
|
| 569 |
+
("resource://test", None), # Missing parameter delimiter
|
| 570 |
+
("resource://other_foo", None), # Wrong prefix
|
| 571 |
+
("other://test_foo", None), # Wrong scheme
|
| 572 |
+
],
|
| 573 |
+
)
|
| 574 |
+
def test_match_uri_template_embedded_param(
|
| 575 |
+
self, uri: str, expected_params: dict[str, str] | None
|
| 576 |
+
):
|
| 577 |
+
"""Test matching URIs where parameter is embedded within a word segment."""
|
| 578 |
+
uri_template = "resource://test_{x}"
|
| 579 |
+
result = match_uri_template(uri=uri, uri_template=uri_template)
|
| 580 |
+
assert result == expected_params
|
| 581 |
+
|
| 582 |
+
@pytest.mark.parametrize(
|
| 583 |
+
"uri, expected_params",
|
| 584 |
+
[
|
| 585 |
+
("resource://prefix_foo_suffix", {"x": "foo"}),
|
| 586 |
+
("resource://prefix_bar_suffix", {"x": "bar"}),
|
| 587 |
+
("resource://prefix_hello_world_suffix", {"x": "hello_world"}),
|
| 588 |
+
("resource://prefix__suffix", None), # Empty parameter not matched
|
| 589 |
+
("resource://prefix_suffix", None), # Missing parameter delimiter
|
| 590 |
+
("resource://other_foo_suffix", None), # Wrong prefix
|
| 591 |
+
("resource://prefix_foo_other", None), # Wrong suffix
|
| 592 |
+
],
|
| 593 |
+
)
|
| 594 |
+
def test_match_uri_template_embedded_param_with_prefix_and_suffix(
|
| 595 |
+
self, uri: str, expected_params: dict[str, str] | None
|
| 596 |
+
):
|
| 597 |
+
"""Test matching URIs where parameter has both prefix and suffix."""
|
| 598 |
+
uri_template = "resource://prefix_{x}_suffix"
|
| 599 |
+
result = match_uri_template(uri=uri, uri_template=uri_template)
|
| 600 |
+
assert result == expected_params
|
| 601 |
+
|
| 602 |
|
| 603 |
class TestContextHandling:
|
| 604 |
"""Test context handling in resource templates."""
|
tests/server/test_server.py
CHANGED
|
@@ -1235,3 +1235,106 @@ class TestResourcePrefixMounting:
|
|
| 1235 |
"resource://imported/param-value/template"
|
| 1236 |
)
|
| 1237 |
assert result[0].text == "Template resource with param-value" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1235 |
"resource://imported/param-value/template"
|
| 1236 |
)
|
| 1237 |
assert result[0].text == "Template resource with param-value" # type: ignore[attr-defined]
|
| 1238 |
+
|
| 1239 |
+
|
| 1240 |
+
class TestShouldIncludeComponent:
|
| 1241 |
+
def test_no_filters_returns_true(self):
|
| 1242 |
+
"""Test that when no include or exclude filters are provided, always returns True."""
|
| 1243 |
+
tool = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1244 |
+
mcp = FastMCP(tools=[tool])
|
| 1245 |
+
result = mcp._should_enable_component(tool)
|
| 1246 |
+
assert result is True
|
| 1247 |
+
|
| 1248 |
+
def test_exclude_string_tag_present_returns_false(self):
|
| 1249 |
+
"""Test that when an exclude string tag is present in tags, returns False."""
|
| 1250 |
+
tool = Tool(
|
| 1251 |
+
name="test_tool", tags={"tag1", "tag2", "exclude_me"}, parameters={}
|
| 1252 |
+
)
|
| 1253 |
+
mcp = FastMCP(tools=[tool], exclude_tags={"exclude_me"})
|
| 1254 |
+
result = mcp._should_enable_component(tool)
|
| 1255 |
+
assert result is False
|
| 1256 |
+
|
| 1257 |
+
def test_exclude_string_tag_absent_returns_true(self):
|
| 1258 |
+
"""Test that when an exclude string tag is not present in tags, returns True."""
|
| 1259 |
+
tool = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1260 |
+
mcp = FastMCP(tools=[tool], exclude_tags={"exclude_me"})
|
| 1261 |
+
result = mcp._should_enable_component(tool)
|
| 1262 |
+
assert result is True
|
| 1263 |
+
|
| 1264 |
+
def test_multiple_exclude_tags_any_match_returns_false(self):
|
| 1265 |
+
"""Test that when any exclude tag matches, returns False."""
|
| 1266 |
+
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
| 1267 |
+
mcp = FastMCP(
|
| 1268 |
+
tools=[tool], exclude_tags={"not_present", "tag2", "also_not_present"}
|
| 1269 |
+
)
|
| 1270 |
+
result = mcp._should_enable_component(tool)
|
| 1271 |
+
assert result is False
|
| 1272 |
+
|
| 1273 |
+
def test_include_string_tag_present_returns_true(self):
|
| 1274 |
+
"""Test that when an include string tag is present in tags, returns True."""
|
| 1275 |
+
tool = Tool(
|
| 1276 |
+
name="test_tool", tags={"tag1", "include_me", "tag2"}, parameters={}
|
| 1277 |
+
)
|
| 1278 |
+
mcp = FastMCP(tools=[tool], include_tags={"include_me"})
|
| 1279 |
+
result = mcp._should_enable_component(tool)
|
| 1280 |
+
assert result is True
|
| 1281 |
+
|
| 1282 |
+
def test_include_string_tag_absent_returns_false(self):
|
| 1283 |
+
"""Test that when an include string tag is not present in tags, returns False."""
|
| 1284 |
+
tool = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1285 |
+
mcp = FastMCP(tools=[tool], include_tags={"include_me"})
|
| 1286 |
+
result = mcp._should_enable_component(tool)
|
| 1287 |
+
assert result is False
|
| 1288 |
+
|
| 1289 |
+
def test_multiple_include_tags_any_match_returns_true(self):
|
| 1290 |
+
"""Test that when any include tag matches, returns True."""
|
| 1291 |
+
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
| 1292 |
+
mcp = FastMCP(
|
| 1293 |
+
tools=[tool], include_tags={"not_present", "tag2", "also_not_present"}
|
| 1294 |
+
)
|
| 1295 |
+
result = mcp._should_enable_component(tool)
|
| 1296 |
+
assert result is True
|
| 1297 |
+
|
| 1298 |
+
def test_multiple_include_tags_none_match_returns_false(self):
|
| 1299 |
+
"""Test that when no include tags match, returns False."""
|
| 1300 |
+
tool = Tool(name="test_tool", tags={"tag1", "tag2", "tag3"}, parameters={})
|
| 1301 |
+
mcp = FastMCP(tools=[tool], include_tags={"not_present", "also_not_present"})
|
| 1302 |
+
result = mcp._should_enable_component(tool)
|
| 1303 |
+
assert result is False
|
| 1304 |
+
|
| 1305 |
+
def test_exclude_takes_precedence_over_include(self):
|
| 1306 |
+
"""Test that exclude tags take precedence over include tags."""
|
| 1307 |
+
tool = Tool(
|
| 1308 |
+
name="test_tool", tags={"tag1", "tag2", "exclude_me"}, parameters={}
|
| 1309 |
+
)
|
| 1310 |
+
mcp = FastMCP(tools=[tool], include_tags={"tag1"}, exclude_tags={"exclude_me"})
|
| 1311 |
+
result = mcp._should_enable_component(tool)
|
| 1312 |
+
assert result is False
|
| 1313 |
+
|
| 1314 |
+
def test_empty_include_exclude_sets(self):
|
| 1315 |
+
"""Test behavior with empty include/exclude sets."""
|
| 1316 |
+
# Empty include set means nothing matches
|
| 1317 |
+
tool1 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1318 |
+
mcp1 = FastMCP(tools=[tool1], include_tags=set())
|
| 1319 |
+
result = mcp1._should_enable_component(tool1)
|
| 1320 |
+
assert result is False
|
| 1321 |
+
|
| 1322 |
+
# Empty exclude set means nothing excluded
|
| 1323 |
+
tool2 = Tool(name="test_tool", tags={"tag1", "tag2"}, parameters={})
|
| 1324 |
+
mcp2 = FastMCP(tools=[tool2], exclude_tags=set())
|
| 1325 |
+
result = mcp2._should_enable_component(tool2)
|
| 1326 |
+
assert result is True
|
| 1327 |
+
|
| 1328 |
+
def test_empty_tags_with_filters(self):
|
| 1329 |
+
"""Test behavior when input tags are empty."""
|
| 1330 |
+
# With include filters, empty tags should not match
|
| 1331 |
+
tool1 = Tool(name="test_tool", tags=set(), parameters={})
|
| 1332 |
+
mcp1 = FastMCP(tools=[tool1], include_tags={"required_tag"})
|
| 1333 |
+
result = mcp1._should_enable_component(tool1)
|
| 1334 |
+
assert result is False
|
| 1335 |
+
|
| 1336 |
+
# With exclude filters but no include, empty tags should pass
|
| 1337 |
+
tool2 = Tool(name="test_tool", tags=set(), parameters={})
|
| 1338 |
+
mcp2 = FastMCP(tools=[tool2], exclude_tags={"bad_tag"})
|
| 1339 |
+
result = mcp2._should_enable_component(tool2)
|
| 1340 |
+
assert result is True
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -115,6 +115,76 @@ class TestTools:
|
|
| 115 |
assert result[0].text == '[\n "x",\n 2\n]' # type: ignore[attr-defined]
|
| 116 |
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
class TestToolReturnTypes:
|
| 119 |
async def test_string(self):
|
| 120 |
mcp = FastMCP()
|
|
@@ -865,6 +935,73 @@ class TestResource:
|
|
| 865 |
assert result[0].blob == base64.b64encode(b"Binary file data").decode() # type: ignore[attr-defined]
|
| 866 |
|
| 867 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
class TestResourceContext:
|
| 869 |
async def test_resource_with_context_annotation_gets_context(self):
|
| 870 |
mcp = FastMCP()
|
|
@@ -1196,6 +1333,76 @@ class TestResourceTemplates:
|
|
| 1196 |
assert result[0].text == "Template resource 1: a/b" # type: ignore[attr-defined]
|
| 1197 |
|
| 1198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
class TestResourceTemplateContext:
|
| 1200 |
async def test_resource_template_context(self):
|
| 1201 |
mcp = FastMCP()
|
|
@@ -1631,3 +1838,73 @@ class TestPromptContext:
|
|
| 1631 |
message = result.messages[0]
|
| 1632 |
assert message.role == "user"
|
| 1633 |
assert message.content.text == "Hello, World! 1" # type: ignore[attr-defined]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
assert result[0].text == '[\n "x",\n 2\n]' # type: ignore[attr-defined]
|
| 116 |
|
| 117 |
|
| 118 |
+
class TestToolTags:
|
| 119 |
+
def create_server(self, include_tags=None, exclude_tags=None):
|
| 120 |
+
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
| 121 |
+
|
| 122 |
+
@mcp.tool(tags={"a", "b"})
|
| 123 |
+
def tool_1() -> int:
|
| 124 |
+
return 1
|
| 125 |
+
|
| 126 |
+
@mcp.tool(tags={"b", "c"})
|
| 127 |
+
def tool_2() -> int:
|
| 128 |
+
return 2
|
| 129 |
+
|
| 130 |
+
return mcp
|
| 131 |
+
|
| 132 |
+
async def test_include_tags_all_tools(self):
|
| 133 |
+
mcp = self.create_server(include_tags={"a", "b"})
|
| 134 |
+
|
| 135 |
+
async with Client(mcp) as client:
|
| 136 |
+
tools = await client.list_tools()
|
| 137 |
+
assert {t.name for t in tools} == {"tool_1", "tool_2"}
|
| 138 |
+
|
| 139 |
+
async def test_include_tags_some_tools(self):
|
| 140 |
+
mcp = self.create_server(include_tags={"a", "z"})
|
| 141 |
+
|
| 142 |
+
async with Client(mcp) as client:
|
| 143 |
+
tools = await client.list_tools()
|
| 144 |
+
assert {t.name for t in tools} == {"tool_1"}
|
| 145 |
+
|
| 146 |
+
async def test_exclude_tags_all_tools(self):
|
| 147 |
+
mcp = self.create_server(exclude_tags={"a", "b"})
|
| 148 |
+
|
| 149 |
+
async with Client(mcp) as client:
|
| 150 |
+
tools = await client.list_tools()
|
| 151 |
+
assert {t.name for t in tools} == set()
|
| 152 |
+
|
| 153 |
+
async def test_exclude_tags_some_tools(self):
|
| 154 |
+
mcp = self.create_server(exclude_tags={"a", "z"})
|
| 155 |
+
|
| 156 |
+
async with Client(mcp) as client:
|
| 157 |
+
tools = await client.list_tools()
|
| 158 |
+
assert {t.name for t in tools} == {"tool_2"}
|
| 159 |
+
|
| 160 |
+
async def test_exclude_precedence(self):
|
| 161 |
+
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
| 162 |
+
|
| 163 |
+
async with Client(mcp) as client:
|
| 164 |
+
tools = await client.list_tools()
|
| 165 |
+
assert {t.name for t in tools} == {"tool_2"}
|
| 166 |
+
|
| 167 |
+
async def test_call_included_tool(self):
|
| 168 |
+
mcp = self.create_server(include_tags={"a"})
|
| 169 |
+
|
| 170 |
+
async with Client(mcp) as client:
|
| 171 |
+
result_1 = await client.call_tool("tool_1", {})
|
| 172 |
+
assert result_1[0].text == "1" # type: ignore[attr-defined]
|
| 173 |
+
|
| 174 |
+
with pytest.raises(ToolError, match="Unknown tool"):
|
| 175 |
+
await client.call_tool("tool_2", {})
|
| 176 |
+
|
| 177 |
+
async def test_call_excluded_tool(self):
|
| 178 |
+
mcp = self.create_server(exclude_tags={"a"})
|
| 179 |
+
|
| 180 |
+
async with Client(mcp) as client:
|
| 181 |
+
with pytest.raises(ToolError, match="Unknown tool"):
|
| 182 |
+
await client.call_tool("tool_1", {})
|
| 183 |
+
|
| 184 |
+
result_2 = await client.call_tool("tool_2", {})
|
| 185 |
+
assert result_2[0].text == "2" # type: ignore[attr-defined]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
class TestToolReturnTypes:
|
| 189 |
async def test_string(self):
|
| 190 |
mcp = FastMCP()
|
|
|
|
| 935 |
assert result[0].blob == base64.b64encode(b"Binary file data").decode() # type: ignore[attr-defined]
|
| 936 |
|
| 937 |
|
| 938 |
+
class TestResourceTags:
|
| 939 |
+
def create_server(self, include_tags=None, exclude_tags=None):
|
| 940 |
+
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
| 941 |
+
|
| 942 |
+
@mcp.resource("resource://1", tags={"a", "b"})
|
| 943 |
+
def resource_1() -> str:
|
| 944 |
+
return "1"
|
| 945 |
+
|
| 946 |
+
@mcp.resource("resource://2", tags={"b", "c"})
|
| 947 |
+
def resource_2() -> str:
|
| 948 |
+
return "2"
|
| 949 |
+
|
| 950 |
+
return mcp
|
| 951 |
+
|
| 952 |
+
async def test_include_tags_all_resources(self):
|
| 953 |
+
mcp = self.create_server(include_tags={"a", "b"})
|
| 954 |
+
|
| 955 |
+
async with Client(mcp) as client:
|
| 956 |
+
resources = await client.list_resources()
|
| 957 |
+
assert {r.name for r in resources} == {"resource_1", "resource_2"}
|
| 958 |
+
|
| 959 |
+
async def test_include_tags_some_resources(self):
|
| 960 |
+
mcp = self.create_server(include_tags={"a", "z"})
|
| 961 |
+
|
| 962 |
+
async with Client(mcp) as client:
|
| 963 |
+
resources = await client.list_resources()
|
| 964 |
+
assert {r.name for r in resources} == {"resource_1"}
|
| 965 |
+
|
| 966 |
+
async def test_exclude_tags_all_resources(self):
|
| 967 |
+
mcp = self.create_server(exclude_tags={"a", "b"})
|
| 968 |
+
|
| 969 |
+
async with Client(mcp) as client:
|
| 970 |
+
resources = await client.list_resources()
|
| 971 |
+
assert {r.name for r in resources} == set()
|
| 972 |
+
|
| 973 |
+
async def test_exclude_tags_some_resources(self):
|
| 974 |
+
mcp = self.create_server(exclude_tags={"a", "z"})
|
| 975 |
+
|
| 976 |
+
async with Client(mcp) as client:
|
| 977 |
+
resources = await client.list_resources()
|
| 978 |
+
assert {r.name for r in resources} == {"resource_2"}
|
| 979 |
+
|
| 980 |
+
async def test_exclude_precedence(self):
|
| 981 |
+
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
| 982 |
+
|
| 983 |
+
async with Client(mcp) as client:
|
| 984 |
+
resources = await client.list_resources()
|
| 985 |
+
assert {r.name for r in resources} == {"resource_2"}
|
| 986 |
+
|
| 987 |
+
async def test_read_included_resource(self):
|
| 988 |
+
mcp = self.create_server(include_tags={"a"})
|
| 989 |
+
|
| 990 |
+
async with Client(mcp) as client:
|
| 991 |
+
result = await client.read_resource(AnyUrl("resource://1"))
|
| 992 |
+
assert result[0].text == "1" # type: ignore[attr-defined]
|
| 993 |
+
|
| 994 |
+
with pytest.raises(McpError, match="Unknown resource"):
|
| 995 |
+
await client.read_resource(AnyUrl("resource://2"))
|
| 996 |
+
|
| 997 |
+
async def test_read_excluded_resource(self):
|
| 998 |
+
mcp = self.create_server(exclude_tags={"a"})
|
| 999 |
+
|
| 1000 |
+
async with Client(mcp) as client:
|
| 1001 |
+
with pytest.raises(McpError, match="Unknown resource"):
|
| 1002 |
+
await client.read_resource(AnyUrl("resource://1"))
|
| 1003 |
+
|
| 1004 |
+
|
| 1005 |
class TestResourceContext:
|
| 1006 |
async def test_resource_with_context_annotation_gets_context(self):
|
| 1007 |
mcp = FastMCP()
|
|
|
|
| 1333 |
assert result[0].text == "Template resource 1: a/b" # type: ignore[attr-defined]
|
| 1334 |
|
| 1335 |
|
| 1336 |
+
class TestResourceTemplatesTags:
|
| 1337 |
+
def create_server(self, include_tags=None, exclude_tags=None):
|
| 1338 |
+
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
| 1339 |
+
|
| 1340 |
+
@mcp.resource("resource://1/{param}", tags={"a", "b"})
|
| 1341 |
+
def template_resource_1(param: str) -> str:
|
| 1342 |
+
return f"Template resource 1: {param}"
|
| 1343 |
+
|
| 1344 |
+
@mcp.resource("resource://2/{param}", tags={"b", "c"})
|
| 1345 |
+
def template_resource_2(param: str) -> str:
|
| 1346 |
+
return f"Template resource 2: {param}"
|
| 1347 |
+
|
| 1348 |
+
return mcp
|
| 1349 |
+
|
| 1350 |
+
async def test_include_tags_all_resources(self):
|
| 1351 |
+
mcp = self.create_server(include_tags={"a", "b"})
|
| 1352 |
+
|
| 1353 |
+
async with Client(mcp) as client:
|
| 1354 |
+
resources = await client.list_resource_templates()
|
| 1355 |
+
assert {r.name for r in resources} == {
|
| 1356 |
+
"template_resource_1",
|
| 1357 |
+
"template_resource_2",
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
async def test_include_tags_some_resources(self):
|
| 1361 |
+
mcp = self.create_server(include_tags={"a"})
|
| 1362 |
+
|
| 1363 |
+
async with Client(mcp) as client:
|
| 1364 |
+
resources = await client.list_resource_templates()
|
| 1365 |
+
assert {r.name for r in resources} == {"template_resource_1"}
|
| 1366 |
+
|
| 1367 |
+
async def test_exclude_tags_all_resources(self):
|
| 1368 |
+
mcp = self.create_server(exclude_tags={"a", "b"})
|
| 1369 |
+
|
| 1370 |
+
async with Client(mcp) as client:
|
| 1371 |
+
resources = await client.list_resource_templates()
|
| 1372 |
+
assert {r.name for r in resources} == set()
|
| 1373 |
+
|
| 1374 |
+
async def test_exclude_tags_some_resources(self):
|
| 1375 |
+
mcp = self.create_server(exclude_tags={"a"})
|
| 1376 |
+
|
| 1377 |
+
async with Client(mcp) as client:
|
| 1378 |
+
resources = await client.list_resource_templates()
|
| 1379 |
+
assert {r.name for r in resources} == {"template_resource_2"}
|
| 1380 |
+
|
| 1381 |
+
async def test_exclude_takes_precedence_over_include(self):
|
| 1382 |
+
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
| 1383 |
+
|
| 1384 |
+
async with Client(mcp) as client:
|
| 1385 |
+
resources = await client.list_resource_templates()
|
| 1386 |
+
assert {r.name for r in resources} == {"template_resource_2"}
|
| 1387 |
+
|
| 1388 |
+
async def test_read_resource_template_includes_tags(self):
|
| 1389 |
+
mcp = self.create_server(include_tags={"a"})
|
| 1390 |
+
|
| 1391 |
+
async with Client(mcp) as client:
|
| 1392 |
+
result = await client.read_resource("resource://1/x")
|
| 1393 |
+
assert result[0].text == "Template resource 1: x" # type: ignore[attr-defined]
|
| 1394 |
+
|
| 1395 |
+
with pytest.raises(McpError, match="Unknown resource"):
|
| 1396 |
+
await client.read_resource("resource://2/x")
|
| 1397 |
+
|
| 1398 |
+
async def test_read_resource_template_excludes_tags(self):
|
| 1399 |
+
mcp = self.create_server(exclude_tags={"a"})
|
| 1400 |
+
|
| 1401 |
+
async with Client(mcp) as client:
|
| 1402 |
+
with pytest.raises(McpError, match="Unknown resource"):
|
| 1403 |
+
await client.read_resource("resource://1/x")
|
| 1404 |
+
|
| 1405 |
+
|
| 1406 |
class TestResourceTemplateContext:
|
| 1407 |
async def test_resource_template_context(self):
|
| 1408 |
mcp = FastMCP()
|
|
|
|
| 1838 |
message = result.messages[0]
|
| 1839 |
assert message.role == "user"
|
| 1840 |
assert message.content.text == "Hello, World! 1" # type: ignore[attr-defined]
|
| 1841 |
+
|
| 1842 |
+
|
| 1843 |
+
class TestPromptTags:
|
| 1844 |
+
def create_server(self, include_tags=None, exclude_tags=None):
|
| 1845 |
+
mcp = FastMCP(include_tags=include_tags, exclude_tags=exclude_tags)
|
| 1846 |
+
|
| 1847 |
+
@mcp.prompt(tags={"a", "b"})
|
| 1848 |
+
def prompt_1() -> str:
|
| 1849 |
+
return "1"
|
| 1850 |
+
|
| 1851 |
+
@mcp.prompt(tags={"b", "c"})
|
| 1852 |
+
def prompt_2() -> str:
|
| 1853 |
+
return "2"
|
| 1854 |
+
|
| 1855 |
+
return mcp
|
| 1856 |
+
|
| 1857 |
+
async def test_include_tags_all_prompts(self):
|
| 1858 |
+
mcp = self.create_server(include_tags={"a", "b"})
|
| 1859 |
+
|
| 1860 |
+
async with Client(mcp) as client:
|
| 1861 |
+
prompts = await client.list_prompts()
|
| 1862 |
+
assert {p.name for p in prompts} == {"prompt_1", "prompt_2"}
|
| 1863 |
+
|
| 1864 |
+
async def test_include_tags_some_prompts(self):
|
| 1865 |
+
mcp = self.create_server(include_tags={"a"})
|
| 1866 |
+
|
| 1867 |
+
async with Client(mcp) as client:
|
| 1868 |
+
prompts = await client.list_prompts()
|
| 1869 |
+
assert {p.name for p in prompts} == {"prompt_1"}
|
| 1870 |
+
|
| 1871 |
+
async def test_exclude_tags_all_prompts(self):
|
| 1872 |
+
mcp = self.create_server(exclude_tags={"a", "b"})
|
| 1873 |
+
|
| 1874 |
+
async with Client(mcp) as client:
|
| 1875 |
+
prompts = await client.list_prompts()
|
| 1876 |
+
assert {p.name for p in prompts} == set()
|
| 1877 |
+
|
| 1878 |
+
async def test_exclude_tags_some_prompts(self):
|
| 1879 |
+
mcp = self.create_server(exclude_tags={"a"})
|
| 1880 |
+
|
| 1881 |
+
async with Client(mcp) as client:
|
| 1882 |
+
prompts = await client.list_prompts()
|
| 1883 |
+
assert {p.name for p in prompts} == {"prompt_2"}
|
| 1884 |
+
|
| 1885 |
+
async def test_exclude_takes_precedence_over_include(self):
|
| 1886 |
+
mcp = self.create_server(exclude_tags={"a"}, include_tags={"b"})
|
| 1887 |
+
|
| 1888 |
+
async with Client(mcp) as client:
|
| 1889 |
+
prompts = await client.list_prompts()
|
| 1890 |
+
assert {p.name for p in prompts} == {"prompt_2"}
|
| 1891 |
+
|
| 1892 |
+
async def test_read_prompt_includes_tags(self):
|
| 1893 |
+
mcp = self.create_server(include_tags={"a"})
|
| 1894 |
+
|
| 1895 |
+
async with Client(mcp) as client:
|
| 1896 |
+
result = await client.get_prompt("prompt_1")
|
| 1897 |
+
assert result.messages[0].content.text == "1" # type: ignore[attr-defined]
|
| 1898 |
+
|
| 1899 |
+
with pytest.raises(McpError, match="Unknown prompt"):
|
| 1900 |
+
await client.get_prompt("prompt_2")
|
| 1901 |
+
|
| 1902 |
+
async def test_read_prompt_excludes_tags(self):
|
| 1903 |
+
mcp = self.create_server(exclude_tags={"a"})
|
| 1904 |
+
|
| 1905 |
+
async with Client(mcp) as client:
|
| 1906 |
+
with pytest.raises(McpError, match="Unknown prompt"):
|
| 1907 |
+
await client.get_prompt("prompt_1")
|
| 1908 |
+
|
| 1909 |
+
result = await client.get_prompt("prompt_2")
|
| 1910 |
+
assert result.messages[0].content.text == "2" # type: ignore[attr-defined]
|