Spaces:
Running
Running
Merge pull request #1281 from jlowin/meta-tag
Browse files- docs/clients/prompts.mdx +25 -0
- docs/clients/resources.mdx +28 -0
- docs/clients/tools.mdx +25 -0
- docs/integrations/openapi.mdx +32 -0
- docs/servers/middleware.mdx +4 -4
- docs/servers/prompts.mdx +1 -1
- docs/servers/resources.mdx +1 -1
- docs/servers/tools.mdx +1 -1
- justfile +1 -1
- src/fastmcp/prompts/prompt.py +1 -0
- src/fastmcp/resources/resource.py +1 -0
- src/fastmcp/resources/template.py +1 -0
- src/fastmcp/tools/tool.py +1 -0
- src/fastmcp/utilities/components.py +10 -1
- tests/server/openapi/test_basic_functionality.py +2 -2
- tests/server/test_server_interactions.py +69 -0
docs/clients/prompts.mdx
CHANGED
|
@@ -25,8 +25,33 @@ async with client:
|
|
| 25 |
print(f"Description: {prompt.description}")
|
| 26 |
if prompt.arguments:
|
| 27 |
print(f"Arguments: {[arg.name for arg in prompt.arguments]}")
|
|
|
|
|
|
|
|
|
|
| 28 |
```
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
## Using Prompts
|
| 31 |
|
| 32 |
### Basic Usage
|
|
|
|
| 25 |
print(f"Description: {prompt.description}")
|
| 26 |
if prompt.arguments:
|
| 27 |
print(f"Arguments: {[arg.name for arg in prompt.arguments]}")
|
| 28 |
+
# Access tags and other metadata
|
| 29 |
+
if hasattr(prompt, '_meta') and prompt._meta:
|
| 30 |
+
print(f"Tags: {prompt._meta.get('tags', [])}")
|
| 31 |
```
|
| 32 |
|
| 33 |
+
### Filtering by Tags
|
| 34 |
+
|
| 35 |
+
You can use the `meta` field to filter prompts based on their tags:
|
| 36 |
+
|
| 37 |
+
```python
|
| 38 |
+
async with client:
|
| 39 |
+
prompts = await client.list_prompts()
|
| 40 |
+
|
| 41 |
+
# Filter prompts by tag
|
| 42 |
+
analysis_prompts = [
|
| 43 |
+
prompt for prompt in prompts
|
| 44 |
+
if hasattr(prompt, '_meta') and prompt._meta and
|
| 45 |
+
'analysis' in prompt._meta.get('tags', [])
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
print(f"Found {len(analysis_prompts)} analysis prompts")
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
<Note>
|
| 52 |
+
The `_meta` field is part of the standard MCP specification, but the inclusion of tags within it is specific to FastMCP servers. Other MCP server implementations may not provide tags in their metadata.
|
| 53 |
+
</Note>
|
| 54 |
+
|
| 55 |
## Using Prompts
|
| 56 |
|
| 57 |
### Basic Usage
|
docs/clients/resources.mdx
CHANGED
|
@@ -34,6 +34,9 @@ async with client:
|
|
| 34 |
print(f"Name: {resource.name}")
|
| 35 |
print(f"Description: {resource.description}")
|
| 36 |
print(f"MIME Type: {resource.mimeType}")
|
|
|
|
|
|
|
|
|
|
| 37 |
```
|
| 38 |
|
| 39 |
### Resource Templates
|
|
@@ -49,8 +52,33 @@ async with client:
|
|
| 49 |
print(f"Template URI: {template.uriTemplate}")
|
| 50 |
print(f"Name: {template.name}")
|
| 51 |
print(f"Description: {template.description}")
|
|
|
|
|
|
|
|
|
|
| 52 |
```
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
## Reading Resources
|
| 55 |
|
| 56 |
### Static Resources
|
|
|
|
| 34 |
print(f"Name: {resource.name}")
|
| 35 |
print(f"Description: {resource.description}")
|
| 36 |
print(f"MIME Type: {resource.mimeType}")
|
| 37 |
+
# Access tags and other metadata
|
| 38 |
+
if hasattr(resource, '_meta') and resource._meta:
|
| 39 |
+
print(f"Tags: {resource._meta.get('tags', [])}")
|
| 40 |
```
|
| 41 |
|
| 42 |
### Resource Templates
|
|
|
|
| 52 |
print(f"Template URI: {template.uriTemplate}")
|
| 53 |
print(f"Name: {template.name}")
|
| 54 |
print(f"Description: {template.description}")
|
| 55 |
+
# Access tags and other metadata
|
| 56 |
+
if hasattr(template, '_meta') and template._meta:
|
| 57 |
+
print(f"Tags: {template._meta.get('tags', [])}")
|
| 58 |
```
|
| 59 |
|
| 60 |
+
### Filtering by Tags
|
| 61 |
+
|
| 62 |
+
You can use the `meta` field to filter resources based on their tags:
|
| 63 |
+
|
| 64 |
+
```python
|
| 65 |
+
async with client:
|
| 66 |
+
resources = await client.list_resources()
|
| 67 |
+
|
| 68 |
+
# Filter resources by tag
|
| 69 |
+
config_resources = [
|
| 70 |
+
resource for resource in resources
|
| 71 |
+
if hasattr(resource, '_meta') and resource._meta and
|
| 72 |
+
'config' in resource._meta.get('tags', [])
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
print(f"Found {len(config_resources)} config resources")
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
<Note>
|
| 79 |
+
The `_meta` field is part of the standard MCP specification, but the inclusion of tags within it is specific to FastMCP servers. Other MCP server implementations may not provide tags in their metadata.
|
| 80 |
+
</Note>
|
| 81 |
+
|
| 82 |
## Reading Resources
|
| 83 |
|
| 84 |
### Static Resources
|
docs/clients/tools.mdx
CHANGED
|
@@ -25,8 +25,33 @@ async with client:
|
|
| 25 |
print(f"Description: {tool.description}")
|
| 26 |
if tool.inputSchema:
|
| 27 |
print(f"Parameters: {tool.inputSchema}")
|
|
|
|
|
|
|
|
|
|
| 28 |
```
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
## Executing Tools
|
| 31 |
|
| 32 |
### Basic Execution
|
|
|
|
| 25 |
print(f"Description: {tool.description}")
|
| 26 |
if tool.inputSchema:
|
| 27 |
print(f"Parameters: {tool.inputSchema}")
|
| 28 |
+
# Access tags and other metadata
|
| 29 |
+
if hasattr(tool, '_meta') and tool._meta:
|
| 30 |
+
print(f"Tags: {tool._meta.get('tags', [])}")
|
| 31 |
```
|
| 32 |
|
| 33 |
+
### Filtering by Tags
|
| 34 |
+
|
| 35 |
+
You can use the `meta` field to filter tools based on their tags:
|
| 36 |
+
|
| 37 |
+
```python
|
| 38 |
+
async with client:
|
| 39 |
+
tools = await client.list_tools()
|
| 40 |
+
|
| 41 |
+
# Filter tools by tag
|
| 42 |
+
analysis_tools = [
|
| 43 |
+
tool for tool in tools
|
| 44 |
+
if hasattr(tool, '_meta') and tool._meta and
|
| 45 |
+
'analysis' in tool._meta.get('tags', [])
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
print(f"Found {len(analysis_tools)} analysis tools")
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
<Note>
|
| 52 |
+
The `_meta` field is part of the standard MCP specification, but the inclusion of tags within it is specific to FastMCP servers. Other MCP server implementations may not provide tags in their metadata.
|
| 53 |
+
</Note>
|
| 54 |
+
|
| 55 |
## Executing Tools
|
| 56 |
|
| 57 |
### Basic Execution
|
docs/integrations/openapi.mdx
CHANGED
|
@@ -332,6 +332,38 @@ mcp = FastMCP.from_openapi(
|
|
| 332 |
)
|
| 333 |
```
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
### Advanced Customization
|
| 336 |
|
| 337 |
<VersionBadge version="2.5.0" />
|
|
|
|
| 332 |
)
|
| 333 |
```
|
| 334 |
|
| 335 |
+
#### OpenAPI Tags in Client Meta
|
| 336 |
+
|
| 337 |
+
FastMCP automatically includes OpenAPI tags from your specification in the component's metadata. These tags are available to MCP clients through the `_meta` field, allowing clients to filter and organize components based on the original OpenAPI tagging:
|
| 338 |
+
|
| 339 |
+
<CodeGroup>
|
| 340 |
+
```json {5} OpenAPI spec with tags
|
| 341 |
+
{
|
| 342 |
+
"paths": {
|
| 343 |
+
"/users": {
|
| 344 |
+
"get": {
|
| 345 |
+
"tags": ["users", "public"],
|
| 346 |
+
"operationId": "list_users",
|
| 347 |
+
"summary": "List all users"
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
```
|
| 353 |
+
```python {6-8} Access OpenAPI tags in MCP client
|
| 354 |
+
async with client:
|
| 355 |
+
tools = await client.list_tools()
|
| 356 |
+
for tool in tools:
|
| 357 |
+
if hasattr(tool, '_meta') and tool._meta:
|
| 358 |
+
# OpenAPI tags are now available!
|
| 359 |
+
openapi_tags = tool._meta.get('tags', [])
|
| 360 |
+
if 'users' in openapi_tags:
|
| 361 |
+
print(f"Found user-related tool: {tool.name}")
|
| 362 |
+
```
|
| 363 |
+
</CodeGroup>
|
| 364 |
+
|
| 365 |
+
This makes it easy for clients to understand and organize API endpoints based on their original OpenAPI categorization.
|
| 366 |
+
|
| 367 |
### Advanced Customization
|
| 368 |
|
| 369 |
<VersionBadge version="2.5.0" />
|
docs/servers/middleware.mdx
CHANGED
|
@@ -111,9 +111,9 @@ FastMCP middleware handles two types of operations differently:
|
|
| 111 |
|
| 112 |
**Listing Operations** (`on_list_tools`, `on_list_resources`, `on_list_prompts`, etc.):
|
| 113 |
- Middleware receives **FastMCP component objects** with full metadata
|
| 114 |
-
- These objects include FastMCP-specific properties like `tags` that
|
| 115 |
-
- The result contains complete component information before it's converted to MCP format
|
| 116 |
-
- Tags
|
| 117 |
|
| 118 |
**Execution Operations** (`on_call_tool`, `on_read_resource`, `on_get_prompt`):
|
| 119 |
- Middleware runs **before** the component is executed
|
|
@@ -200,7 +200,7 @@ class ListingFilterMiddleware(Middleware):
|
|
| 200 |
return filtered_tools
|
| 201 |
```
|
| 202 |
|
| 203 |
-
This filtering happens before the components are converted to MCP format and returned to the client
|
| 204 |
|
| 205 |
<Tip>
|
| 206 |
When filtering components in listing operations, ensure you also prevent execution of filtered components in the corresponding execution hooks (`on_call_tool`, `on_read_resource`, `on_get_prompt`) to maintain consistency.
|
|
|
|
| 111 |
|
| 112 |
**Listing Operations** (`on_list_tools`, `on_list_resources`, `on_list_prompts`, etc.):
|
| 113 |
- Middleware receives **FastMCP component objects** with full metadata
|
| 114 |
+
- These objects include FastMCP-specific properties like `tags` that can be accessed directly from the component
|
| 115 |
+
- The result contains complete component information before it's converted to MCP format
|
| 116 |
+
- Tags are included in the component's `meta` field in the listing response returned to MCP clients
|
| 117 |
|
| 118 |
**Execution Operations** (`on_call_tool`, `on_read_resource`, `on_get_prompt`):
|
| 119 |
- Middleware runs **before** the component is executed
|
|
|
|
| 200 |
return filtered_tools
|
| 201 |
```
|
| 202 |
|
| 203 |
+
This filtering happens before the components are converted to MCP format and returned to the client. Tags are accessible both during filtering and are included in the component's `meta` field in the final listing response.
|
| 204 |
|
| 205 |
<Tip>
|
| 206 |
When filtering components in listing operations, ensure you also prevent execution of filtered components in the corresponding execution hooks (`on_call_tool`, `on_read_resource`, `on_get_prompt`) to maintain consistency.
|
docs/servers/prompts.mdx
CHANGED
|
@@ -85,7 +85,7 @@ def data_analysis_prompt(
|
|
| 85 |
</ParamField>
|
| 86 |
|
| 87 |
<ParamField body="tags" type="set[str] | None">
|
| 88 |
-
A set of strings used to categorize the prompt. Clients might use tags to filter or group available prompts
|
| 89 |
</ParamField>
|
| 90 |
|
| 91 |
<ParamField body="enabled" type="bool" default="True">
|
|
|
|
| 85 |
</ParamField>
|
| 86 |
|
| 87 |
<ParamField body="tags" type="set[str] | None">
|
| 88 |
+
A set of strings used to categorize the prompt. Clients might use tags to filter or group available prompts. Tags are available to clients in the prompt's `meta` field when the prompt is listed (via `list_prompts`)
|
| 89 |
</ParamField>
|
| 90 |
|
| 91 |
<ParamField body="enabled" type="bool" default="True">
|
docs/servers/resources.mdx
CHANGED
|
@@ -98,7 +98,7 @@ def get_application_status() -> dict:
|
|
| 98 |
</ParamField>
|
| 99 |
|
| 100 |
<ParamField body="tags" type="set[str] | None">
|
| 101 |
-
A set of strings for categorization, potentially used by clients for filtering
|
| 102 |
</ParamField>
|
| 103 |
|
| 104 |
<ParamField body="enabled" type="bool" default="True">
|
|
|
|
| 98 |
</ParamField>
|
| 99 |
|
| 100 |
<ParamField body="tags" type="set[str] | None">
|
| 101 |
+
A set of strings for categorization, potentially used by clients for filtering. Tags are available to clients in the resource's `meta` field when the resource is listed (via `list_resources` or `list_resource_templates`)
|
| 102 |
</ParamField>
|
| 103 |
|
| 104 |
<ParamField body="enabled" type="bool" default="True">
|
docs/servers/tools.mdx
CHANGED
|
@@ -76,7 +76,7 @@ def search_products_implementation(query: str, category: str | None = None) -> l
|
|
| 76 |
</ParamField>
|
| 77 |
|
| 78 |
<ParamField body="tags" type="set[str] | None">
|
| 79 |
-
A set of strings to categorize the tool. Clients might use tags to filter or group available tools
|
| 80 |
</ParamField>
|
| 81 |
|
| 82 |
<ParamField body="enabled" type="bool" default="True">
|
|
|
|
| 76 |
</ParamField>
|
| 77 |
|
| 78 |
<ParamField body="tags" type="set[str] | None">
|
| 79 |
+
A set of strings to categorize the tool. Clients might use tags to filter or group available tools. Tags are available to clients in the tool's `meta` field when the tool is listed (via `list_tools`)
|
| 80 |
</ParamField>
|
| 81 |
|
| 82 |
<ParamField body="enabled" type="bool" default="True">
|
justfile
CHANGED
|
@@ -12,7 +12,7 @@ typecheck:
|
|
| 12 |
|
| 13 |
# Serve documentation locally
|
| 14 |
docs:
|
| 15 |
-
cd docs && npx mint@latest dev
|
| 16 |
|
| 17 |
# Generate API reference documentation for all modules
|
| 18 |
api-ref-all:
|
|
|
|
| 12 |
|
| 13 |
# Serve documentation locally
|
| 14 |
docs:
|
| 15 |
+
cd docs && npx --yes mint@latest dev
|
| 16 |
|
| 17 |
# Generate API reference documentation for all modules
|
| 18 |
api-ref-all:
|
src/fastmcp/prompts/prompt.py
CHANGED
|
@@ -100,6 +100,7 @@ class Prompt(FastMCPComponent, ABC):
|
|
| 100 |
"description": self.description,
|
| 101 |
"arguments": arguments,
|
| 102 |
"title": self.title,
|
|
|
|
| 103 |
}
|
| 104 |
return MCPPrompt(**kwargs | overrides)
|
| 105 |
|
|
|
|
| 100 |
"description": self.description,
|
| 101 |
"arguments": arguments,
|
| 102 |
"title": self.title,
|
| 103 |
+
"_meta": self.get_meta(),
|
| 104 |
}
|
| 105 |
return MCPPrompt(**kwargs | overrides)
|
| 106 |
|
src/fastmcp/resources/resource.py
CHANGED
|
@@ -122,6 +122,7 @@ class Resource(FastMCPComponent, abc.ABC):
|
|
| 122 |
"mimeType": self.mime_type,
|
| 123 |
"title": self.title,
|
| 124 |
"annotations": self.annotations,
|
|
|
|
| 125 |
}
|
| 126 |
return MCPResource(**kwargs | overrides)
|
| 127 |
|
|
|
|
| 122 |
"mimeType": self.mime_type,
|
| 123 |
"title": self.title,
|
| 124 |
"annotations": self.annotations,
|
| 125 |
+
"_meta": self.get_meta(),
|
| 126 |
}
|
| 127 |
return MCPResource(**kwargs | overrides)
|
| 128 |
|
src/fastmcp/resources/template.py
CHANGED
|
@@ -154,6 +154,7 @@ class ResourceTemplate(FastMCPComponent):
|
|
| 154 |
"mimeType": self.mime_type,
|
| 155 |
"title": self.title,
|
| 156 |
"annotations": self.annotations,
|
|
|
|
| 157 |
}
|
| 158 |
return MCPResourceTemplate(**kwargs | overrides)
|
| 159 |
|
|
|
|
| 154 |
"mimeType": self.mime_type,
|
| 155 |
"title": self.title,
|
| 156 |
"annotations": self.annotations,
|
| 157 |
+
"_meta": self.get_meta(),
|
| 158 |
}
|
| 159 |
return MCPResourceTemplate(**kwargs | overrides)
|
| 160 |
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -145,6 +145,7 @@ class Tool(FastMCPComponent):
|
|
| 145 |
"outputSchema": self.output_schema,
|
| 146 |
"annotations": self.annotations,
|
| 147 |
"title": title,
|
|
|
|
| 148 |
}
|
| 149 |
return MCPTool(**kwargs | overrides)
|
| 150 |
|
|
|
|
| 145 |
"outputSchema": self.output_schema,
|
| 146 |
"annotations": self.annotations,
|
| 147 |
"title": title,
|
| 148 |
+
"_meta": self.get_meta(),
|
| 149 |
}
|
| 150 |
return MCPTool(**kwargs | overrides)
|
| 151 |
|
src/fastmcp/utilities/components.py
CHANGED
|
@@ -36,7 +36,9 @@ class FastMCPComponent(FastMCPBaseModel):
|
|
| 36 |
default_factory=set,
|
| 37 |
description="Tags for the component.",
|
| 38 |
)
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
enabled: bool = Field(
|
| 41 |
default=True,
|
| 42 |
description="Whether the component is enabled.",
|
|
@@ -58,6 +60,13 @@ class FastMCPComponent(FastMCPBaseModel):
|
|
| 58 |
"""
|
| 59 |
return self._key or self.name
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def with_key(self, key: str) -> Self:
|
| 62 |
return self.model_copy(update={"_key": key})
|
| 63 |
|
|
|
|
| 36 |
default_factory=set,
|
| 37 |
description="Tags for the component.",
|
| 38 |
)
|
| 39 |
+
meta: dict[str, Any] = Field(
|
| 40 |
+
default_factory=dict, description="Meta information about the prompt"
|
| 41 |
+
)
|
| 42 |
enabled: bool = Field(
|
| 43 |
default=True,
|
| 44 |
description="Whether the component is enabled.",
|
|
|
|
| 60 |
"""
|
| 61 |
return self._key or self.name
|
| 62 |
|
| 63 |
+
def get_meta(self) -> dict[str, Any]:
|
| 64 |
+
"""Get the meta information about the component."""
|
| 65 |
+
if self.tags:
|
| 66 |
+
return {"tags": sorted(self.tags)} | self.meta
|
| 67 |
+
else:
|
| 68 |
+
return self.meta
|
| 69 |
+
|
| 70 |
def with_key(self, key: str) -> Self:
|
| 71 |
return self.model_copy(update={"_key": key})
|
| 72 |
|
tests/server/openapi/test_basic_functionality.py
CHANGED
|
@@ -97,7 +97,7 @@ class TestTools:
|
|
| 97 |
|
| 98 |
assert tools[0].model_dump() == dict(
|
| 99 |
name="create_user_users_post",
|
| 100 |
-
meta=
|
| 101 |
title=None,
|
| 102 |
annotations=None,
|
| 103 |
description=IsStr(regex=r"^Create a new user\..*$", regex_flags=re.DOTALL),
|
|
@@ -122,7 +122,7 @@ class TestTools:
|
|
| 122 |
)
|
| 123 |
assert tools[1].model_dump() == dict(
|
| 124 |
name="update_user_name_users",
|
| 125 |
-
meta=
|
| 126 |
title=None,
|
| 127 |
annotations=None,
|
| 128 |
description=IsStr(
|
|
|
|
| 97 |
|
| 98 |
assert tools[0].model_dump() == dict(
|
| 99 |
name="create_user_users_post",
|
| 100 |
+
meta=dict(tags=["create", "users"]),
|
| 101 |
title=None,
|
| 102 |
annotations=None,
|
| 103 |
description=IsStr(regex=r"^Create a new user\..*$", regex_flags=re.DOTALL),
|
|
|
|
| 122 |
)
|
| 123 |
assert tools[1].model_dump() == dict(
|
| 124 |
name="update_user_name_users",
|
| 125 |
+
meta=dict(tags=["update", "users"]),
|
| 126 |
title=None,
|
| 127 |
annotations=None,
|
| 128 |
description=IsStr(
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -268,6 +268,21 @@ class TestToolTags:
|
|
| 268 |
result_2 = await client.call_tool("tool_2", {})
|
| 269 |
assert result_2.data == 2
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
class TestToolReturnTypes:
|
| 273 |
async def test_string(self):
|
|
@@ -1544,6 +1559,26 @@ class TestResourceTags:
|
|
| 1544 |
with pytest.raises(McpError, match="Unknown resource"):
|
| 1545 |
await client.read_resource(AnyUrl("resource://1"))
|
| 1546 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1547 |
|
| 1548 |
class TestResourceContext:
|
| 1549 |
async def test_resource_with_context_annotation_gets_context(self):
|
|
@@ -1972,6 +2007,26 @@ class TestResourceTemplatesTags:
|
|
| 1972 |
result = await client.read_resource("resource://2/x")
|
| 1973 |
assert result[0].text == "Template resource 2: x" # type: ignore[attr-defined]
|
| 1974 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1975 |
|
| 1976 |
class TestResourceTemplateContext:
|
| 1977 |
async def test_resource_template_context(self):
|
|
@@ -2339,6 +2394,20 @@ class TestPrompts:
|
|
| 2339 |
prompt = prompts_dict["sample_prompt"]
|
| 2340 |
assert prompt.tags == {"example", "test-tag"}
|
| 2341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2342 |
|
| 2343 |
class TestPromptEnabled:
|
| 2344 |
async def test_toggle_enabled(self):
|
|
|
|
| 268 |
result_2 = await client.call_tool("tool_2", {})
|
| 269 |
assert result_2.data == 2
|
| 270 |
|
| 271 |
+
async def test_tags_in_meta(self):
|
| 272 |
+
mcp = FastMCP()
|
| 273 |
+
|
| 274 |
+
@mcp.tool(tags={"tool-example", "test-tool-tag"})
|
| 275 |
+
def sample_tool(x: int) -> int:
|
| 276 |
+
"""A sample tool."""
|
| 277 |
+
return x * 2
|
| 278 |
+
|
| 279 |
+
async with Client(mcp) as client:
|
| 280 |
+
tools = await client.list_tools()
|
| 281 |
+
assert len(tools) == 1
|
| 282 |
+
tool = tools[0]
|
| 283 |
+
assert tool.meta is not None
|
| 284 |
+
assert set(tool.meta["tags"]) == {"tool-example", "test-tool-tag"}
|
| 285 |
+
|
| 286 |
|
| 287 |
class TestToolReturnTypes:
|
| 288 |
async def test_string(self):
|
|
|
|
| 1559 |
with pytest.raises(McpError, match="Unknown resource"):
|
| 1560 |
await client.read_resource(AnyUrl("resource://1"))
|
| 1561 |
|
| 1562 |
+
async def test_tags_in_meta(self):
|
| 1563 |
+
mcp = FastMCP()
|
| 1564 |
+
|
| 1565 |
+
@mcp.resource(
|
| 1566 |
+
uri="test://resource", tags={"resource-example", "test-resource-tag"}
|
| 1567 |
+
)
|
| 1568 |
+
def sample_resource() -> str:
|
| 1569 |
+
"""A sample resource."""
|
| 1570 |
+
return "resource content"
|
| 1571 |
+
|
| 1572 |
+
async with Client(mcp) as client:
|
| 1573 |
+
resources = await client.list_resources()
|
| 1574 |
+
assert len(resources) == 1
|
| 1575 |
+
resource = resources[0]
|
| 1576 |
+
assert resource.meta is not None
|
| 1577 |
+
assert set(resource.meta["tags"]) == {
|
| 1578 |
+
"resource-example",
|
| 1579 |
+
"test-resource-tag",
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
|
| 1583 |
class TestResourceContext:
|
| 1584 |
async def test_resource_with_context_annotation_gets_context(self):
|
|
|
|
| 2007 |
result = await client.read_resource("resource://2/x")
|
| 2008 |
assert result[0].text == "Template resource 2: x" # type: ignore[attr-defined]
|
| 2009 |
|
| 2010 |
+
async def test_tags_in_meta(self):
|
| 2011 |
+
mcp = FastMCP()
|
| 2012 |
+
|
| 2013 |
+
@mcp.resource(
|
| 2014 |
+
"test://template/{id}", tags={"template-example", "test-template-tag"}
|
| 2015 |
+
)
|
| 2016 |
+
def sample_template(id: str) -> str:
|
| 2017 |
+
"""A sample resource template."""
|
| 2018 |
+
return f"template content for {id}"
|
| 2019 |
+
|
| 2020 |
+
async with Client(mcp) as client:
|
| 2021 |
+
templates = await client.list_resource_templates()
|
| 2022 |
+
assert len(templates) == 1
|
| 2023 |
+
template = templates[0]
|
| 2024 |
+
assert template.meta is not None
|
| 2025 |
+
assert set(template.meta["tags"]) == {
|
| 2026 |
+
"template-example",
|
| 2027 |
+
"test-template-tag",
|
| 2028 |
+
}
|
| 2029 |
+
|
| 2030 |
|
| 2031 |
class TestResourceTemplateContext:
|
| 2032 |
async def test_resource_template_context(self):
|
|
|
|
| 2394 |
prompt = prompts_dict["sample_prompt"]
|
| 2395 |
assert prompt.tags == {"example", "test-tag"}
|
| 2396 |
|
| 2397 |
+
async def test_tags_in_meta(self):
|
| 2398 |
+
mcp = FastMCP()
|
| 2399 |
+
|
| 2400 |
+
@mcp.prompt(tags={"example", "test-tag"})
|
| 2401 |
+
def sample_prompt() -> str:
|
| 2402 |
+
return "Hello, world!"
|
| 2403 |
+
|
| 2404 |
+
async with Client(mcp) as client:
|
| 2405 |
+
prompts = await client.list_prompts()
|
| 2406 |
+
assert len(prompts) == 1
|
| 2407 |
+
prompt = prompts[0]
|
| 2408 |
+
assert prompt.meta is not None
|
| 2409 |
+
assert set(prompt.meta["tags"]) == {"example", "test-tag"}
|
| 2410 |
+
|
| 2411 |
|
| 2412 |
class TestPromptEnabled:
|
| 2413 |
async def test_toggle_enabled(self):
|