Jeremiah Lowin commited on
Commit
0a39b15
·
unverified ·
2 Parent(s): 3a7947fed43295

Merge pull request #1281 from jlowin/meta-tag

Browse files
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 aren't part of the MCP specification
115
- - The result contains complete component information before it's converted to MCP format
116
- - Tags and other metadata are stripped when finally returned to the MCP client
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, so the tags (which are FastMCP-specific) are naturally stripped in the final 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.
 
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=None,
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=None,
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):