Jeremiah Lowin Claude commited on
Commit
226fdf7
·
1 Parent(s): 8ca70c5

Add title field support to FastMCP components

Browse files

- Add optional title field to FastMCPComponent base class
- Support title parameter in all component decorators (@tool , @resource , @prompt )
- Include title in MCP protocol serialization when provided
- Add get_display_name() method to prefer title over name for display
- Add comprehensive tests for title functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

src/fastmcp/prompts/prompt.py CHANGED
@@ -100,12 +100,16 @@ class Prompt(FastMCPComponent, ABC):
100
  "description": self.description,
101
  "arguments": arguments,
102
  }
 
 
 
103
  return MCPPrompt(**kwargs | overrides)
104
 
105
  @staticmethod
106
  def from_function(
107
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
108
  name: str | None = None,
 
109
  description: str | None = None,
110
  tags: set[str] | None = None,
111
  enabled: bool | None = None,
@@ -119,7 +123,12 @@ class Prompt(FastMCPComponent, ABC):
119
  - A sequence of any of the above
120
  """
121
  return FunctionPrompt.from_function(
122
- fn=fn, name=name, description=description, tags=tags, enabled=enabled
 
 
 
 
 
123
  )
124
 
125
  @abstractmethod
@@ -141,6 +150,7 @@ class FunctionPrompt(Prompt):
141
  cls,
142
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
143
  name: str | None = None,
 
144
  description: str | None = None,
145
  tags: set[str] | None = None,
146
  enabled: bool | None = None,
@@ -232,6 +242,7 @@ class FunctionPrompt(Prompt):
232
 
233
  return cls(
234
  name=func_name,
 
235
  description=description,
236
  arguments=arguments,
237
  tags=tags or set(),
 
100
  "description": self.description,
101
  "arguments": arguments,
102
  }
103
+ # Add title field if provided
104
+ if self.title is not None:
105
+ kwargs["title"] = self.title
106
  return MCPPrompt(**kwargs | overrides)
107
 
108
  @staticmethod
109
  def from_function(
110
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
111
  name: str | None = None,
112
+ title: str | None = None,
113
  description: str | None = None,
114
  tags: set[str] | None = None,
115
  enabled: bool | None = None,
 
123
  - A sequence of any of the above
124
  """
125
  return FunctionPrompt.from_function(
126
+ fn=fn,
127
+ name=name,
128
+ title=title,
129
+ description=description,
130
+ tags=tags,
131
+ enabled=enabled,
132
  )
133
 
134
  @abstractmethod
 
150
  cls,
151
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
152
  name: str | None = None,
153
+ title: str | None = None,
154
  description: str | None = None,
155
  tags: set[str] | None = None,
156
  enabled: bool | None = None,
 
242
 
243
  return cls(
244
  name=func_name,
245
+ title=title,
246
  description=description,
247
  arguments=arguments,
248
  tags=tags or set(),
src/fastmcp/resources/resource.py CHANGED
@@ -65,6 +65,7 @@ class Resource(FastMCPComponent, abc.ABC):
65
  fn: Callable[[], Any],
66
  uri: str | AnyUrl,
67
  name: str | None = None,
 
68
  description: str | None = None,
69
  mime_type: str | None = None,
70
  tags: set[str] | None = None,
@@ -74,6 +75,7 @@ class Resource(FastMCPComponent, abc.ABC):
74
  fn=fn,
75
  uri=uri,
76
  name=name,
 
77
  description=description,
78
  mime_type=mime_type,
79
  tags=tags,
@@ -112,6 +114,9 @@ class Resource(FastMCPComponent, abc.ABC):
112
  "description": self.description,
113
  "mimeType": self.mime_type,
114
  }
 
 
 
115
  return MCPResource(**kwargs | overrides)
116
 
117
  def __repr__(self) -> str:
@@ -149,6 +154,7 @@ class FunctionResource(Resource):
149
  fn: Callable[[], Any],
150
  uri: str | AnyUrl,
151
  name: str | None = None,
 
152
  description: str | None = None,
153
  mime_type: str | None = None,
154
  tags: set[str] | None = None,
@@ -161,6 +167,7 @@ class FunctionResource(Resource):
161
  fn=fn,
162
  uri=uri,
163
  name=name or fn.__name__,
 
164
  description=description or inspect.getdoc(fn),
165
  mime_type=mime_type or "text/plain",
166
  tags=tags or set(),
 
65
  fn: Callable[[], Any],
66
  uri: str | AnyUrl,
67
  name: str | None = None,
68
+ title: str | None = None,
69
  description: str | None = None,
70
  mime_type: str | None = None,
71
  tags: set[str] | None = None,
 
75
  fn=fn,
76
  uri=uri,
77
  name=name,
78
+ title=title,
79
  description=description,
80
  mime_type=mime_type,
81
  tags=tags,
 
114
  "description": self.description,
115
  "mimeType": self.mime_type,
116
  }
117
+ # Add title field if provided
118
+ if self.title is not None:
119
+ kwargs["title"] = self.title
120
  return MCPResource(**kwargs | overrides)
121
 
122
  def __repr__(self) -> str:
 
154
  fn: Callable[[], Any],
155
  uri: str | AnyUrl,
156
  name: str | None = None,
157
+ title: str | None = None,
158
  description: str | None = None,
159
  mime_type: str | None = None,
160
  tags: set[str] | None = None,
 
167
  fn=fn,
168
  uri=uri,
169
  name=name or fn.__name__,
170
+ title=title,
171
  description=description or inspect.getdoc(fn),
172
  mime_type=mime_type or "text/plain",
173
  tags=tags or set(),
src/fastmcp/resources/template.py CHANGED
@@ -86,6 +86,7 @@ class ResourceTemplate(FastMCPComponent):
86
  fn: Callable[..., Any],
87
  uri_template: str,
88
  name: str | None = None,
 
89
  description: str | None = None,
90
  mime_type: str | None = None,
91
  tags: set[str] | None = None,
@@ -95,6 +96,7 @@ class ResourceTemplate(FastMCPComponent):
95
  fn=fn,
96
  uri_template=uri_template,
97
  name=name,
 
98
  description=description,
99
  mime_type=mime_type,
100
  tags=tags,
@@ -145,6 +147,9 @@ class ResourceTemplate(FastMCPComponent):
145
  "description": self.description,
146
  "mimeType": self.mime_type,
147
  }
 
 
 
148
  return MCPResourceTemplate(**kwargs | overrides)
149
 
150
  @classmethod
@@ -197,6 +202,7 @@ class FunctionResourceTemplate(ResourceTemplate):
197
  fn: Callable[..., Any],
198
  uri_template: str,
199
  name: str | None = None,
 
200
  description: str | None = None,
201
  mime_type: str | None = None,
202
  tags: set[str] | None = None,
@@ -278,6 +284,7 @@ class FunctionResourceTemplate(ResourceTemplate):
278
  return cls(
279
  uri_template=uri_template,
280
  name=func_name,
 
281
  description=description,
282
  mime_type=mime_type or "text/plain",
283
  fn=fn,
 
86
  fn: Callable[..., Any],
87
  uri_template: str,
88
  name: str | None = None,
89
+ title: str | None = None,
90
  description: str | None = None,
91
  mime_type: str | None = None,
92
  tags: set[str] | None = None,
 
96
  fn=fn,
97
  uri_template=uri_template,
98
  name=name,
99
+ title=title,
100
  description=description,
101
  mime_type=mime_type,
102
  tags=tags,
 
147
  "description": self.description,
148
  "mimeType": self.mime_type,
149
  }
150
+ # Add title field if provided
151
+ if self.title is not None:
152
+ kwargs["title"] = self.title
153
  return MCPResourceTemplate(**kwargs | overrides)
154
 
155
  @classmethod
 
202
  fn: Callable[..., Any],
203
  uri_template: str,
204
  name: str | None = None,
205
+ title: str | None = None,
206
  description: str | None = None,
207
  mime_type: str | None = None,
208
  tags: set[str] | None = None,
 
284
  return cls(
285
  uri_template=uri_template,
286
  name=func_name,
287
+ title=title,
288
  description=description,
289
  mime_type=mime_type or "text/plain",
290
  fn=fn,
src/fastmcp/server/server.py CHANGED
@@ -790,6 +790,7 @@ class FastMCP(Generic[LifespanResultT]):
790
  name_or_fn: AnyFunction,
791
  *,
792
  name: str | None = None,
 
793
  description: str | None = None,
794
  tags: set[str] | None = None,
795
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
@@ -804,6 +805,7 @@ class FastMCP(Generic[LifespanResultT]):
804
  name_or_fn: str | None = None,
805
  *,
806
  name: str | None = None,
 
807
  description: str | None = None,
808
  tags: set[str] | None = None,
809
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
@@ -817,6 +819,7 @@ class FastMCP(Generic[LifespanResultT]):
817
  name_or_fn: str | AnyFunction | None = None,
818
  *,
819
  name: str | None = None,
 
820
  description: str | None = None,
821
  tags: set[str] | None = None,
822
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
@@ -897,6 +900,7 @@ class FastMCP(Generic[LifespanResultT]):
897
  tool = Tool.from_function(
898
  fn,
899
  name=tool_name,
 
900
  description=description,
901
  tags=tags,
902
  output_schema=output_schema,
@@ -928,6 +932,7 @@ class FastMCP(Generic[LifespanResultT]):
928
  return partial(
929
  self.tool,
930
  name=tool_name,
 
931
  description=description,
932
  tags=tags,
933
  output_schema=output_schema,
@@ -1016,6 +1021,7 @@ class FastMCP(Generic[LifespanResultT]):
1016
  uri: str,
1017
  *,
1018
  name: str | None = None,
 
1019
  description: str | None = None,
1020
  mime_type: str | None = None,
1021
  tags: set[str] | None = None,
@@ -1107,6 +1113,7 @@ class FastMCP(Generic[LifespanResultT]):
1107
  fn=fn,
1108
  uri_template=uri,
1109
  name=name,
 
1110
  description=description,
1111
  mime_type=mime_type,
1112
  tags=tags,
@@ -1119,6 +1126,7 @@ class FastMCP(Generic[LifespanResultT]):
1119
  fn=fn,
1120
  uri=uri,
1121
  name=name,
 
1122
  description=description,
1123
  mime_type=mime_type,
1124
  tags=tags,
@@ -1158,6 +1166,7 @@ class FastMCP(Generic[LifespanResultT]):
1158
  name_or_fn: AnyFunction,
1159
  *,
1160
  name: str | None = None,
 
1161
  description: str | None = None,
1162
  tags: set[str] | None = None,
1163
  enabled: bool | None = None,
@@ -1169,6 +1178,7 @@ class FastMCP(Generic[LifespanResultT]):
1169
  name_or_fn: str | None = None,
1170
  *,
1171
  name: str | None = None,
 
1172
  description: str | None = None,
1173
  tags: set[str] | None = None,
1174
  enabled: bool | None = None,
@@ -1179,6 +1189,7 @@ class FastMCP(Generic[LifespanResultT]):
1179
  name_or_fn: str | AnyFunction | None = None,
1180
  *,
1181
  name: str | None = None,
 
1182
  description: str | None = None,
1183
  tags: set[str] | None = None,
1184
  enabled: bool | None = None,
@@ -1275,6 +1286,7 @@ class FastMCP(Generic[LifespanResultT]):
1275
  prompt = Prompt.from_function(
1276
  fn=fn,
1277
  name=prompt_name,
 
1278
  description=description,
1279
  tags=tags,
1280
  enabled=enabled,
@@ -1303,6 +1315,7 @@ class FastMCP(Generic[LifespanResultT]):
1303
  return partial(
1304
  self.prompt,
1305
  name=prompt_name,
 
1306
  description=description,
1307
  tags=tags,
1308
  enabled=enabled,
 
790
  name_or_fn: AnyFunction,
791
  *,
792
  name: str | None = None,
793
+ title: str | None = None,
794
  description: str | None = None,
795
  tags: set[str] | None = None,
796
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
 
805
  name_or_fn: str | None = None,
806
  *,
807
  name: str | None = None,
808
+ title: str | None = None,
809
  description: str | None = None,
810
  tags: set[str] | None = None,
811
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
 
819
  name_or_fn: str | AnyFunction | None = None,
820
  *,
821
  name: str | None = None,
822
+ title: str | None = None,
823
  description: str | None = None,
824
  tags: set[str] | None = None,
825
  output_schema: dict[str, Any] | None | NotSetT = NotSet,
 
900
  tool = Tool.from_function(
901
  fn,
902
  name=tool_name,
903
+ title=title,
904
  description=description,
905
  tags=tags,
906
  output_schema=output_schema,
 
932
  return partial(
933
  self.tool,
934
  name=tool_name,
935
+ title=title,
936
  description=description,
937
  tags=tags,
938
  output_schema=output_schema,
 
1021
  uri: str,
1022
  *,
1023
  name: str | None = None,
1024
+ title: str | None = None,
1025
  description: str | None = None,
1026
  mime_type: str | None = None,
1027
  tags: set[str] | None = None,
 
1113
  fn=fn,
1114
  uri_template=uri,
1115
  name=name,
1116
+ title=title,
1117
  description=description,
1118
  mime_type=mime_type,
1119
  tags=tags,
 
1126
  fn=fn,
1127
  uri=uri,
1128
  name=name,
1129
+ title=title,
1130
  description=description,
1131
  mime_type=mime_type,
1132
  tags=tags,
 
1166
  name_or_fn: AnyFunction,
1167
  *,
1168
  name: str | None = None,
1169
+ title: str | None = None,
1170
  description: str | None = None,
1171
  tags: set[str] | None = None,
1172
  enabled: bool | None = None,
 
1178
  name_or_fn: str | None = None,
1179
  *,
1180
  name: str | None = None,
1181
+ title: str | None = None,
1182
  description: str | None = None,
1183
  tags: set[str] | None = None,
1184
  enabled: bool | None = None,
 
1189
  name_or_fn: str | AnyFunction | None = None,
1190
  *,
1191
  name: str | None = None,
1192
+ title: str | None = None,
1193
  description: str | None = None,
1194
  tags: set[str] | None = None,
1195
  enabled: bool | None = None,
 
1286
  prompt = Prompt.from_function(
1287
  fn=fn,
1288
  name=prompt_name,
1289
+ title=title,
1290
  description=description,
1291
  tags=tags,
1292
  enabled=enabled,
 
1315
  return partial(
1316
  self.prompt,
1317
  name=prompt_name,
1318
+ title=title,
1319
  description=description,
1320
  tags=tags,
1321
  enabled=enabled,
src/fastmcp/tools/tool.py CHANGED
@@ -142,12 +142,16 @@ class Tool(FastMCPComponent):
142
  "outputSchema": self.output_schema,
143
  "annotations": self.annotations,
144
  }
 
 
 
145
  return MCPTool(**kwargs | overrides)
146
 
147
  @staticmethod
148
  def from_function(
149
  fn: Callable[..., Any],
150
  name: str | None = None,
 
151
  description: str | None = None,
152
  tags: set[str] | None = None,
153
  annotations: ToolAnnotations | None = None,
@@ -160,6 +164,7 @@ class Tool(FastMCPComponent):
160
  return FunctionTool.from_function(
161
  fn=fn,
162
  name=name,
 
163
  description=description,
164
  tags=tags,
165
  annotations=annotations,
@@ -219,6 +224,7 @@ class FunctionTool(Tool):
219
  cls,
220
  fn: Callable[..., Any],
221
  name: str | None = None,
 
222
  description: str | None = None,
223
  tags: set[str] | None = None,
224
  annotations: ToolAnnotations | None = None,
@@ -250,6 +256,7 @@ class FunctionTool(Tool):
250
  return cls(
251
  fn=parsed_fn.fn,
252
  name=name or parsed_fn.name,
 
253
  description=description or parsed_fn.description,
254
  parameters=parsed_fn.input_schema,
255
  output_schema=output_schema,
 
142
  "outputSchema": self.output_schema,
143
  "annotations": self.annotations,
144
  }
145
+ # Add title field if provided
146
+ if self.title is not None:
147
+ kwargs["title"] = self.title
148
  return MCPTool(**kwargs | overrides)
149
 
150
  @staticmethod
151
  def from_function(
152
  fn: Callable[..., Any],
153
  name: str | None = None,
154
+ title: str | None = None,
155
  description: str | None = None,
156
  tags: set[str] | None = None,
157
  annotations: ToolAnnotations | None = None,
 
164
  return FunctionTool.from_function(
165
  fn=fn,
166
  name=name,
167
+ title=title,
168
  description=description,
169
  tags=tags,
170
  annotations=annotations,
 
224
  cls,
225
  fn: Callable[..., Any],
226
  name: str | None = None,
227
+ title: str | None = None,
228
  description: str | None = None,
229
  tags: set[str] | None = None,
230
  annotations: ToolAnnotations | None = None,
 
256
  return cls(
257
  fn=parsed_fn.fn,
258
  name=name or parsed_fn.name,
259
+ title=title,
260
  description=description or parsed_fn.description,
261
  parameters=parsed_fn.input_schema,
262
  output_schema=output_schema,
src/fastmcp/utilities/components.py CHANGED
@@ -24,6 +24,10 @@ class FastMCPComponent(FastMCPBaseModel):
24
  name: str = Field(
25
  description="The name of the component.",
26
  )
 
 
 
 
27
  description: str | None = Field(
28
  default=None,
29
  description="The description of the component.",
@@ -64,7 +68,7 @@ class FastMCPComponent(FastMCPBaseModel):
64
  return self.model_dump() == other.model_dump()
65
 
66
  def __repr__(self) -> str:
67
- return f"{self.__class__.__name__}(name={self.name!r}, description={self.description!r}, tags={self.tags}, enabled={self.enabled})"
68
 
69
  def enable(self) -> None:
70
  """Enable the component."""
@@ -73,3 +77,7 @@ class FastMCPComponent(FastMCPBaseModel):
73
  def disable(self) -> None:
74
  """Disable the component."""
75
  self.enabled = False
 
 
 
 
 
24
  name: str = Field(
25
  description="The name of the component.",
26
  )
27
+ title: str | None = Field(
28
+ default=None,
29
+ description="The title of the component for display purposes.",
30
+ )
31
  description: str | None = Field(
32
  default=None,
33
  description="The description of the component.",
 
68
  return self.model_dump() == other.model_dump()
69
 
70
  def __repr__(self) -> str:
71
+ return f"{self.__class__.__name__}(name={self.name!r}, title={self.title!r}, description={self.description!r}, tags={self.tags}, enabled={self.enabled})"
72
 
73
  def enable(self) -> None:
74
  """Enable the component."""
 
77
  def disable(self) -> None:
78
  """Disable the component."""
79
  self.enabled = False
80
+
81
+ def get_display_name(self) -> str:
82
+ """Get the display name for this component, preferring title over name."""
83
+ return self.title if self.title is not None else self.name
tests/tools/test_tool.py CHANGED
@@ -1225,3 +1225,50 @@ class TestAutomaticStructuredContent:
1225
  assert result.data.name == "Bob"
1226
  assert result.data.age == 25
1227
  assert result.data.verified is True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1225
  assert result.data.name == "Bob"
1226
  assert result.data.age == 25
1227
  assert result.data.verified is True
1228
+
1229
+
1230
+ class TestToolTitle:
1231
+ """Tests for tool title functionality."""
1232
+
1233
+ def test_tool_with_title(self):
1234
+ """Test that tools can have titles and they appear in MCP conversion."""
1235
+
1236
+ def calculate(x: int, y: int) -> int:
1237
+ """Calculate the sum of two numbers."""
1238
+ return x + y
1239
+
1240
+ tool = Tool.from_function(
1241
+ calculate,
1242
+ name="calc",
1243
+ title="Advanced Calculator Tool",
1244
+ description="Custom description",
1245
+ )
1246
+
1247
+ assert tool.name == "calc"
1248
+ assert tool.title == "Advanced Calculator Tool"
1249
+ assert tool.description == "Custom description"
1250
+ assert tool.get_display_name() == "Advanced Calculator Tool"
1251
+
1252
+ # Test MCP conversion includes title
1253
+ mcp_tool = tool.to_mcp_tool()
1254
+ assert mcp_tool.name == "calc"
1255
+ assert (
1256
+ hasattr(mcp_tool, "title") and mcp_tool.title == "Advanced Calculator Tool"
1257
+ )
1258
+
1259
+ def test_tool_without_title(self):
1260
+ """Test that tools without titles use name as display name."""
1261
+
1262
+ def multiply(a: int, b: int) -> int:
1263
+ return a * b
1264
+
1265
+ tool = Tool.from_function(multiply)
1266
+
1267
+ assert tool.name == "multiply"
1268
+ assert tool.title is None
1269
+ assert tool.get_display_name() == "multiply"
1270
+
1271
+ # Test MCP conversion doesn't include title when None
1272
+ mcp_tool = tool.to_mcp_tool()
1273
+ assert mcp_tool.name == "multiply"
1274
+ assert not hasattr(mcp_tool, "title") or mcp_tool.title is None