Jeremiah Lowin commited on
Commit
27260c2
·
unverified ·
2 Parent(s): 2b1d891e98b7a0

Merge pull request #982 from jlowin/title-field-support

Browse files
src/fastmcp/prompts/prompt.py CHANGED
@@ -99,6 +99,7 @@ class Prompt(FastMCPComponent, ABC):
99
  "name": self.name,
100
  "description": self.description,
101
  "arguments": arguments,
 
102
  }
103
  return MCPPrompt(**kwargs | overrides)
104
 
@@ -106,6 +107,7 @@ class Prompt(FastMCPComponent, ABC):
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 +121,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 +148,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 +240,7 @@ class FunctionPrompt(Prompt):
232
 
233
  return cls(
234
  name=func_name,
 
235
  description=description,
236
  arguments=arguments,
237
  tags=tags or set(),
 
99
  "name": self.name,
100
  "description": self.description,
101
  "arguments": arguments,
102
+ "title": self.title,
103
  }
104
  return MCPPrompt(**kwargs | overrides)
105
 
 
107
  def from_function(
108
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
109
  name: str | None = None,
110
+ title: str | None = None,
111
  description: str | None = None,
112
  tags: set[str] | None = None,
113
  enabled: bool | None = None,
 
121
  - A sequence of any of the above
122
  """
123
  return FunctionPrompt.from_function(
124
+ fn=fn,
125
+ name=name,
126
+ title=title,
127
+ description=description,
128
+ tags=tags,
129
+ enabled=enabled,
130
  )
131
 
132
  @abstractmethod
 
148
  cls,
149
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
150
  name: str | None = None,
151
+ title: str | None = None,
152
  description: str | None = None,
153
  tags: set[str] | None = None,
154
  enabled: bool | None = None,
 
240
 
241
  return cls(
242
  name=func_name,
243
+ title=title,
244
  description=description,
245
  arguments=arguments,
246
  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,
@@ -111,6 +113,7 @@ class Resource(FastMCPComponent, abc.ABC):
111
  "name": self.name,
112
  "description": self.description,
113
  "mimeType": self.mime_type,
 
114
  }
115
  return MCPResource(**kwargs | overrides)
116
 
@@ -149,6 +152,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 +165,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,
 
113
  "name": self.name,
114
  "description": self.description,
115
  "mimeType": self.mime_type,
116
+ "title": self.title,
117
  }
118
  return MCPResource(**kwargs | overrides)
119
 
 
152
  fn: Callable[[], Any],
153
  uri: str | AnyUrl,
154
  name: str | None = None,
155
+ title: str | None = None,
156
  description: str | None = None,
157
  mime_type: str | None = None,
158
  tags: set[str] | None = None,
 
165
  fn=fn,
166
  uri=uri,
167
  name=name or fn.__name__,
168
+ title=title,
169
  description=description or inspect.getdoc(fn),
170
  mime_type=mime_type or "text/plain",
171
  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,
@@ -144,6 +146,7 @@ class ResourceTemplate(FastMCPComponent):
144
  "name": self.name,
145
  "description": self.description,
146
  "mimeType": self.mime_type,
 
147
  }
148
  return MCPResourceTemplate(**kwargs | overrides)
149
 
@@ -197,6 +200,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 +282,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,
 
146
  "name": self.name,
147
  "description": self.description,
148
  "mimeType": self.mime_type,
149
+ "title": self.title,
150
  }
151
  return MCPResourceTemplate(**kwargs | overrides)
152
 
 
200
  fn: Callable[..., Any],
201
  uri_template: str,
202
  name: str | None = None,
203
+ title: str | None = None,
204
  description: str | None = None,
205
  mime_type: str | None = None,
206
  tags: set[str] | None = None,
 
282
  return cls(
283
  uri_template=uri_template,
284
  name=func_name,
285
+ title=title,
286
  description=description,
287
  mime_type=mime_type or "text/plain",
288
  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
@@ -135,12 +135,20 @@ class Tool(FastMCPComponent):
135
  pass # No context available
136
 
137
  def to_mcp_tool(self, **overrides: Any) -> MCPTool:
 
 
 
 
 
 
 
138
  kwargs = {
139
  "name": self.name,
140
  "description": self.description,
141
  "inputSchema": self.parameters,
142
  "outputSchema": self.output_schema,
143
  "annotations": self.annotations,
 
144
  }
145
  return MCPTool(**kwargs | overrides)
146
 
@@ -148,6 +156,7 @@ class Tool(FastMCPComponent):
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 +169,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 +229,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 +261,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,
 
135
  pass # No context available
136
 
137
  def to_mcp_tool(self, **overrides: Any) -> MCPTool:
138
+ if self.title:
139
+ title = self.title
140
+ elif self.annotations and self.annotations.title:
141
+ title = self.annotations.title
142
+ else:
143
+ title = None
144
+
145
  kwargs = {
146
  "name": self.name,
147
  "description": self.description,
148
  "inputSchema": self.parameters,
149
  "outputSchema": self.output_schema,
150
  "annotations": self.annotations,
151
+ "title": title,
152
  }
153
  return MCPTool(**kwargs | overrides)
154
 
 
156
  def from_function(
157
  fn: Callable[..., Any],
158
  name: str | None = None,
159
+ title: str | None = None,
160
  description: str | None = None,
161
  tags: set[str] | None = None,
162
  annotations: ToolAnnotations | None = None,
 
169
  return FunctionTool.from_function(
170
  fn=fn,
171
  name=name,
172
+ title=title,
173
  description=description,
174
  tags=tags,
175
  annotations=annotations,
 
229
  cls,
230
  fn: Callable[..., Any],
231
  name: str | None = None,
232
+ title: str | None = None,
233
  description: str | None = None,
234
  tags: set[str] | None = None,
235
  annotations: ToolAnnotations | None = None,
 
261
  return cls(
262
  fn=parsed_fn.fn,
263
  name=name or parsed_fn.name,
264
+ title=title,
265
  description=description or parsed_fn.description,
266
  parameters=parsed_fn.input_schema,
267
  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,97 @@ 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
+
1251
+ # Test MCP conversion includes title
1252
+ mcp_tool = tool.to_mcp_tool()
1253
+ assert mcp_tool.name == "calc"
1254
+ assert (
1255
+ hasattr(mcp_tool, "title") and mcp_tool.title == "Advanced Calculator Tool"
1256
+ )
1257
+
1258
+ def test_tool_without_title(self):
1259
+ """Test that tools without titles use name as display name."""
1260
+
1261
+ def multiply(a: int, b: int) -> int:
1262
+ return a * b
1263
+
1264
+ tool = Tool.from_function(multiply)
1265
+
1266
+ assert tool.name == "multiply"
1267
+ assert tool.title is None
1268
+
1269
+ # Test MCP conversion doesn't include title when None
1270
+ mcp_tool = tool.to_mcp_tool()
1271
+ assert mcp_tool.name == "multiply"
1272
+ assert not hasattr(mcp_tool, "title") or mcp_tool.title is None
1273
+
1274
+ def test_tool_title_priority(self):
1275
+ """Test that explicit title takes priority over annotations.title."""
1276
+ from mcp.types import ToolAnnotations
1277
+
1278
+ def divide(x: int, y: int) -> float:
1279
+ """Divide two numbers."""
1280
+ return x / y
1281
+
1282
+ # Test with both explicit title and annotations.title
1283
+ annotations = ToolAnnotations(title="Annotation Title")
1284
+ tool = Tool.from_function(
1285
+ divide,
1286
+ name="div",
1287
+ title="Explicit Title",
1288
+ annotations=annotations,
1289
+ )
1290
+
1291
+ assert tool.title == "Explicit Title"
1292
+ assert tool.annotations is not None
1293
+ assert tool.annotations.title == "Annotation Title"
1294
+
1295
+ # Explicit title should take priority
1296
+ mcp_tool = tool.to_mcp_tool()
1297
+ assert mcp_tool.title == "Explicit Title"
1298
+
1299
+ def test_tool_annotations_title_fallback(self):
1300
+ """Test that annotations.title is used when no explicit title is provided."""
1301
+ from mcp.types import ToolAnnotations
1302
+
1303
+ def modulo(x: int, y: int) -> int:
1304
+ """Get modulo of two numbers."""
1305
+ return x % y
1306
+
1307
+ # Test with only annotations.title (no explicit title)
1308
+ annotations = ToolAnnotations(title="Annotation Title")
1309
+ tool = Tool.from_function(
1310
+ modulo,
1311
+ name="mod",
1312
+ annotations=annotations,
1313
+ )
1314
+
1315
+ assert tool.title is None
1316
+ assert tool.annotations is not None
1317
+ assert tool.annotations.title == "Annotation Title"
1318
+
1319
+ # Should fall back to annotations.title
1320
+ mcp_tool = tool.to_mcp_tool()
1321
+ assert mcp_tool.title == "Annotation Title"