fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -26,7 +26,6 @@ class ToolInfo:
26
26
  output_schema: dict[str, Any] | None = None
27
27
  annotations: dict[str, Any] | None = None
28
28
  tags: list[str] | None = None
29
- enabled: bool | None = None
30
29
  title: str | None = None
31
30
  icons: list[dict[str, Any]] | None = None
32
31
  meta: dict[str, Any] | None = None
@@ -41,7 +40,6 @@ class PromptInfo:
41
40
  description: str | None
42
41
  arguments: list[dict[str, Any]] | None = None
43
42
  tags: list[str] | None = None
44
- enabled: bool | None = None
45
43
  title: str | None = None
46
44
  icons: list[dict[str, Any]] | None = None
47
45
  meta: dict[str, Any] | None = None
@@ -58,7 +56,6 @@ class ResourceInfo:
58
56
  mime_type: str | None = None
59
57
  annotations: dict[str, Any] | None = None
60
58
  tags: list[str] | None = None
61
- enabled: bool | None = None
62
59
  title: str | None = None
63
60
  icons: list[dict[str, Any]] | None = None
64
61
  meta: dict[str, Any] | None = None
@@ -76,7 +73,6 @@ class TemplateInfo:
76
73
  parameters: dict[str, Any] | None = None
77
74
  annotations: dict[str, Any] | None = None
78
75
  tags: list[str] | None = None
79
- enabled: bool | None = None
80
76
  title: str | None = None
81
77
  icons: list[dict[str, Any]] | None = None
82
78
  meta: dict[str, Any] | None = None
@@ -110,16 +106,16 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
110
106
  Returns:
111
107
  FastMCPInfo dataclass containing the extracted information
112
108
  """
113
- # Get all components via middleware to respect filtering and preserve metadata
114
- tools_list = await mcp._list_tools_middleware()
115
- prompts_list = await mcp._list_prompts_middleware()
116
- resources_list = await mcp._list_resources_middleware()
117
- templates_list = await mcp._list_resource_templates_middleware()
109
+ # Get all components (list_* includes middleware, enabled/auth filtering)
110
+ tools_list = await mcp.list_tools()
111
+ prompts_list = await mcp.list_prompts()
112
+ resources_list = await mcp.list_resources()
113
+ templates_list = await mcp.list_resource_templates()
118
114
 
119
115
  # Extract detailed tool information
120
116
  tool_infos = []
121
117
  for tool in tools_list:
122
- mcp_tool = tool.to_mcp_tool(name=tool.key)
118
+ mcp_tool = tool.to_mcp_tool(name=tool.name)
123
119
  tool_infos.append(
124
120
  ToolInfo(
125
121
  key=tool.key,
@@ -129,7 +125,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
129
125
  output_schema=tool.output_schema,
130
126
  annotations=tool.annotations.model_dump() if tool.annotations else None,
131
127
  tags=list(tool.tags) if tool.tags else None,
132
- enabled=tool.enabled,
133
128
  title=tool.title,
134
129
  icons=[icon.model_dump() for icon in tool.icons]
135
130
  if tool.icons
@@ -150,7 +145,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
150
145
  if prompt.arguments
151
146
  else None,
152
147
  tags=list(prompt.tags) if prompt.tags else None,
153
- enabled=prompt.enabled,
154
148
  title=prompt.title,
155
149
  icons=[icon.model_dump() for icon in prompt.icons]
156
150
  if prompt.icons
@@ -165,7 +159,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
165
159
  resource_infos.append(
166
160
  ResourceInfo(
167
161
  key=resource.key,
168
- uri=resource.key,
162
+ uri=str(resource.uri),
169
163
  name=resource.name,
170
164
  description=resource.description,
171
165
  mime_type=resource.mime_type,
@@ -173,7 +167,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
173
167
  if resource.annotations
174
168
  else None,
175
169
  tags=list(resource.tags) if resource.tags else None,
176
- enabled=resource.enabled,
177
170
  title=resource.title,
178
171
  icons=[icon.model_dump() for icon in resource.icons]
179
172
  if resource.icons
@@ -188,7 +181,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
188
181
  template_infos.append(
189
182
  TemplateInfo(
190
183
  key=template.key,
191
- uri_template=template.key,
184
+ uri_template=template.uri_template,
192
185
  name=template.name,
193
186
  description=template.description,
194
187
  mime_type=template.mime_type,
@@ -197,7 +190,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
197
190
  if template.annotations
198
191
  else None,
199
192
  tags=list(template.tags) if template.tags else None,
200
- enabled=template.enabled,
201
193
  title=template.title,
202
194
  icons=[icon.model_dump() for icon in template.icons]
203
195
  if template.icons
@@ -275,7 +267,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
275
267
  output_schema=None, # v1 doesn't have output_schema
276
268
  annotations=None, # v1 doesn't have annotations
277
269
  tags=None, # v1 doesn't have tags
278
- enabled=None, # v1 doesn't have enabled field
279
270
  title=None, # v1 doesn't have title
280
271
  icons=[icon.model_dump() for icon in mcp_tool.icons]
281
272
  if hasattr(mcp_tool, "icons") and mcp_tool.icons
@@ -299,7 +290,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
299
290
  description=mcp_prompt.description,
300
291
  arguments=arguments,
301
292
  tags=None, # v1 doesn't have tags
302
- enabled=None, # v1 doesn't have enabled field
303
293
  title=None, # v1 doesn't have title
304
294
  icons=[icon.model_dump() for icon in mcp_prompt.icons]
305
295
  if hasattr(mcp_prompt, "icons") and mcp_prompt.icons
@@ -320,7 +310,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
320
310
  mime_type=mcp_resource.mimeType,
321
311
  annotations=None, # v1 doesn't have annotations
322
312
  tags=None, # v1 doesn't have tags
323
- enabled=None, # v1 doesn't have enabled field
324
313
  title=None, # v1 doesn't have title
325
314
  icons=[icon.model_dump() for icon in mcp_resource.icons]
326
315
  if hasattr(mcp_resource, "icons") and mcp_resource.icons
@@ -342,7 +331,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
342
331
  parameters=None, # v1 doesn't expose template parameters
343
332
  annotations=None, # v1 doesn't have annotations
344
333
  tags=None, # v1 doesn't have tags
345
- enabled=None, # v1 doesn't have enabled field
346
334
  title=None, # v1 doesn't have title
347
335
  icons=[icon.model_dump() for icon in mcp_template.icons]
348
336
  if hasattr(mcp_template, "icons") and mcp_template.icons
@@ -364,31 +364,38 @@ def _single_pass_optimize(
364
364
  def compress_schema(
365
365
  schema: dict[str, Any],
366
366
  prune_params: list[str] | None = None,
367
- prune_defs: bool = True,
368
367
  prune_additional_properties: bool = True,
369
368
  prune_titles: bool = False,
370
369
  ) -> dict[str, Any]:
371
370
  """
372
- Remove the given parameters from the schema.
371
+ Compress and optimize a JSON schema for MCP compatibility.
372
+
373
+ This function dereferences all $ref entries (inlining definitions) to ensure
374
+ compatibility with MCP clients that don't properly handle $ref in schemas
375
+ (e.g., VS Code Copilot). It also applies various optimizations to reduce
376
+ schema size.
373
377
 
374
378
  Args:
375
379
  schema: The schema to compress
376
380
  prune_params: List of parameter names to remove from properties
377
- prune_defs: Whether to remove unused definitions
378
381
  prune_additional_properties: Whether to remove additionalProperties: false
379
382
  prune_titles: Whether to remove title fields from the schema
380
383
  """
384
+ # Dereference $ref - this inlines all definitions and removes $defs
385
+ # Required for MCP client compatibility
386
+ schema = dereference_refs(schema)
387
+
381
388
  # Remove specific parameters if requested
382
389
  for param in prune_params or []:
383
390
  schema = _prune_param(schema, param=param)
384
391
 
385
392
  # Apply combined optimizations in a single tree traversal
386
- if prune_titles or prune_additional_properties or prune_defs:
393
+ if prune_titles or prune_additional_properties:
387
394
  schema = _single_pass_optimize(
388
395
  schema,
389
396
  prune_titles=prune_titles,
390
397
  prune_additional_properties=prune_additional_properties,
391
- prune_defs=prune_defs,
398
+ prune_defs=False,
392
399
  )
393
400
 
394
401
  return schema
@@ -47,6 +47,7 @@ from typing import (
47
47
  ForwardRef,
48
48
  Literal,
49
49
  Union,
50
+ cast,
50
51
  )
51
52
 
52
53
  from pydantic import (
@@ -171,14 +172,15 @@ def json_schema_to_type(
171
172
  if not schema.get("properties") and schema.get("additionalProperties"):
172
173
  additional_props = schema["additionalProperties"]
173
174
  if additional_props is True:
174
- return dict[str, Any] # type: ignore - additionalProperties: true means dict[str, Any]
175
+ return dict[str, Any]
175
176
  else:
176
177
  # Handle typed dictionaries like dict[str, str]
177
178
  value_type = _schema_to_type(additional_props, schemas=schema)
178
- return dict[str, value_type] # type: ignore
179
+ # value_type might be ForwardRef or type - cast to Any for dynamic type construction
180
+ return cast(type[Any], dict[str, value_type]) # type: ignore[valid-type]
179
181
  # If no properties and no additionalProperties, default to dict[str, Any] for safety
180
182
  elif not schema.get("properties") and not schema.get("additionalProperties"):
181
- return dict[str, Any] # type: ignore
183
+ return dict[str, Any]
182
184
  # If has properties AND additionalProperties is True, use Pydantic BaseModel
183
185
  elif schema.get("properties") and schema.get("additionalProperties") is True:
184
186
  return _create_pydantic_model(schema, name, schemas=schema)
@@ -265,13 +267,13 @@ def _create_array_type(
265
267
  if isinstance(items, list):
266
268
  # Handle positional item schemas
267
269
  item_types = [_schema_to_type(s, schemas) for s in items]
268
- combined = Union[tuple(item_types)] # type: ignore[arg-type] # noqa: UP007
270
+ combined = Union[tuple(item_types)] # noqa: UP007
269
271
  base = list[combined] # type: ignore[valid-type]
270
272
  else:
271
273
  # Handle single item schema
272
274
  item_type = _schema_to_type(items, schemas)
273
275
  base_class = set if schema.get("uniqueItems") else list
274
- base = base_class[item_type] # type: ignore[misc]
276
+ base = base_class[item_type]
275
277
 
276
278
  constraints = {
277
279
  k: v
@@ -295,17 +297,17 @@ def _get_from_type_handler(
295
297
  """Get the appropriate type handler for the schema."""
296
298
 
297
299
  type_handlers: dict[str, Callable[..., Any]] = { # TODO
298
- "string": lambda s: _create_string_type(s), # type: ignore
299
- "integer": lambda s: _create_numeric_type(int, s), # type: ignore
300
- "number": lambda s: _create_numeric_type(float, s), # type: ignore
301
- "boolean": lambda _: bool, # type: ignore
302
- "null": lambda _: type(None), # type: ignore
303
- "array": lambda s: _create_array_type(s, schemas), # type: ignore
300
+ "string": lambda s: _create_string_type(s),
301
+ "integer": lambda s: _create_numeric_type(int, s),
302
+ "number": lambda s: _create_numeric_type(float, s),
303
+ "boolean": lambda _: bool,
304
+ "null": lambda _: type(None),
305
+ "array": lambda s: _create_array_type(s, schemas),
304
306
  "object": lambda s: (
305
307
  _create_pydantic_model(s, s.get("title"), schemas)
306
308
  if s.get("properties") and s.get("additionalProperties") is True
307
309
  else _create_dataclass(s, s.get("title"), schemas)
308
- ), # type: ignore
310
+ ),
309
311
  }
310
312
  return type_handlers.get(schema.get("type", None), _return_Any)
311
313
 
@@ -326,7 +328,7 @@ def _schema_to_type(
326
328
  ref = schema["$ref"]
327
329
  # Handle self-reference
328
330
  if ref == "#":
329
- return ForwardRef(schema.get("title", "Root")) # type: ignore[return-value]
331
+ return ForwardRef(schema.get("title", "Root"))
330
332
  return _schema_to_type(_resolve_ref(ref, schemas), schemas)
331
333
 
332
334
  if "const" in schema:
@@ -348,7 +350,7 @@ def _schema_to_type(
348
350
  # This is a dict type, handle it directly
349
351
  additional_props = subschema["additionalProperties"]
350
352
  if additional_props is True:
351
- types.append(dict[str, Any]) # type: ignore
353
+ types.append(dict[str, Any])
352
354
  else:
353
355
  value_type = _schema_to_type(additional_props, schemas)
354
356
  types.append(dict[str, value_type]) # type: ignore
@@ -374,7 +376,7 @@ def _schema_to_type(
374
376
 
375
377
  schema_type = schema.get("type")
376
378
  if not schema_type:
377
- return Any # type: ignore[return-value]
379
+ return Any
378
380
 
379
381
  if isinstance(schema_type, list):
380
382
  # Create a copy of the schema for each type, but keep all constraints
@@ -0,0 +1,56 @@
1
+ """Lifespan utilities for combining async context manager lifespans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator, Callable
6
+ from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
7
+ from typing import Any, TypeVar
8
+
9
+ AppT = TypeVar("AppT")
10
+
11
+
12
+ def combine_lifespans(
13
+ *lifespans: Callable[[AppT], AbstractAsyncContextManager[dict[str, Any] | None]],
14
+ ) -> Callable[[AppT], AbstractAsyncContextManager[dict[str, Any]]]:
15
+ """Combine multiple lifespans into a single lifespan.
16
+
17
+ Useful when mounting FastMCP into FastAPI and you need to run
18
+ both your app's lifespan and the MCP server's lifespan.
19
+
20
+ Works with both FastAPI-style lifespans (yield None) and FastMCP-style
21
+ lifespans (yield dict). Results are merged; later lifespans override
22
+ earlier ones on key conflicts.
23
+
24
+ Lifespans are entered in order and exited in reverse order (LIFO).
25
+
26
+ Example:
27
+ ```python
28
+ from fastmcp import FastMCP
29
+ from fastmcp.utilities.lifespan import combine_lifespans
30
+ from fastapi import FastAPI
31
+
32
+ mcp = FastMCP("Tools")
33
+ mcp_app = mcp.http_app()
34
+
35
+ app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))
36
+ app.mount("/mcp", mcp_app) # MCP endpoint at /mcp
37
+ ```
38
+
39
+ Args:
40
+ *lifespans: Lifespan context manager factories to combine.
41
+
42
+ Returns:
43
+ A combined lifespan context manager factory.
44
+ """
45
+
46
+ @asynccontextmanager
47
+ async def combined(app: AppT) -> AsyncIterator[dict[str, Any]]:
48
+ merged: dict[str, Any] = {}
49
+ async with AsyncExitStack() as stack:
50
+ for ls in lifespans:
51
+ result = await stack.enter_async_context(ls(app))
52
+ if result is not None:
53
+ merged.update(result)
54
+ yield merged
55
+
56
+ return combined
@@ -57,6 +57,18 @@ def configure_logging(
57
57
  logger.propagate = False
58
58
  logger.setLevel(level)
59
59
 
60
+ # Remove any existing handlers to avoid duplicates on reconfiguration
61
+ for hdlr in logger.handlers[:]:
62
+ logger.removeHandler(hdlr)
63
+
64
+ # Use standard logging handlers if rich logging is disabled
65
+ if not fastmcp.settings.enable_rich_logging:
66
+ # Create a standard StreamHandler for stderr
67
+ handler = logging.StreamHandler()
68
+ handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
69
+ logger.addHandler(handler)
70
+ return
71
+
60
72
  # Configure the handler for normal logs
61
73
  handler = RichHandler(
62
74
  console=Console(stderr=True),
@@ -91,10 +103,6 @@ def configure_logging(
91
103
 
92
104
  traceback_handler.addFilter(lambda record: record.exc_info is not None)
93
105
 
94
- # Remove any existing handlers to avoid duplicates on reconfiguration
95
- for hdlr in logger.handlers[:]:
96
- logger.removeHandler(hdlr)
97
-
98
106
  logger.addHandler(handler)
99
107
  logger.addHandler(traceback_handler)
100
108
 
@@ -216,7 +216,7 @@ class MCPServerConfig(BaseModel):
216
216
 
217
217
  """
218
218
  if isinstance(v, dict):
219
- return Deployment(**v) # type: ignore[arg-type]
219
+ return Deployment(**v)
220
220
  return cast(Deployment, v) # type: ignore[return-value]
221
221
 
222
222
  @classmethod
@@ -301,9 +301,9 @@ class MCPServerConfig(BaseModel):
301
301
  if any([transport, host, port, path, log_level, env, cwd, args]):
302
302
  # Convert streamable-http to http for backward compatibility
303
303
  if transport == "streamable-http":
304
- transport = "http" # type: ignore[assignment]
304
+ transport = "http"
305
305
  deployment = Deployment(
306
- transport=transport, # type: ignore[arg-type]
306
+ transport=transport,
307
307
  host=host,
308
308
  port=port,
309
309
  path=path,
@@ -1,6 +1,6 @@
1
1
  """OpenAPI parsing logic for converting OpenAPI specs to HTTPRoute objects."""
2
2
 
3
- from typing import Any, Generic, TypeVar
3
+ from typing import Any, Generic, TypeVar, cast
4
4
 
5
5
  from openapi_pydantic import (
6
6
  OpenAPI,
@@ -146,9 +146,9 @@ class OpenAPIParser(
146
146
  def _convert_to_parameter_location(self, param_in: str) -> ParameterLocation:
147
147
  """Convert string parameter location to our ParameterLocation type."""
148
148
  if param_in in ["path", "query", "header", "cookie"]:
149
- return param_in # type: ignore[return-value] # Safe cast since we checked values
149
+ return cast(ParameterLocation, param_in)
150
150
  logger.warning(f"Unknown parameter location: {param_in}, defaulting to 'query'")
151
- return "query" # type: ignore[return-value] # Safe cast to default value
151
+ return cast(ParameterLocation, "query")
152
152
 
153
153
  def _resolve_ref(self, item: Any) -> Any:
154
154
  """Resolves a reference to its target definition."""
@@ -0,0 +1,80 @@
1
+ """Pagination utilities for MCP list operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import binascii
7
+ import json
8
+ from collections.abc import Sequence
9
+ from dataclasses import dataclass
10
+ from typing import TypeVar
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ @dataclass
16
+ class CursorState:
17
+ """Internal representation of pagination cursor state.
18
+
19
+ The cursor encodes the offset into the result set. This is opaque to clients
20
+ per the MCP spec - they should not parse or modify cursors.
21
+ """
22
+
23
+ offset: int
24
+
25
+ def encode(self) -> str:
26
+ """Encode cursor state to an opaque string."""
27
+ data = json.dumps({"o": self.offset})
28
+ return base64.urlsafe_b64encode(data.encode()).decode()
29
+
30
+ @classmethod
31
+ def decode(cls, cursor: str) -> CursorState:
32
+ """Decode cursor from an opaque string.
33
+
34
+ Raises:
35
+ ValueError: If the cursor is invalid or malformed.
36
+ """
37
+ try:
38
+ data = json.loads(base64.urlsafe_b64decode(cursor.encode()).decode())
39
+ return cls(offset=data["o"])
40
+ except (
41
+ json.JSONDecodeError,
42
+ KeyError,
43
+ ValueError,
44
+ TypeError,
45
+ binascii.Error,
46
+ ) as e:
47
+ raise ValueError(f"Invalid cursor: {cursor}") from e
48
+
49
+
50
+ def paginate_sequence(
51
+ items: Sequence[T],
52
+ cursor: str | None,
53
+ page_size: int,
54
+ ) -> tuple[list[T], str | None]:
55
+ """Paginate a sequence of items.
56
+
57
+ Args:
58
+ items: The full sequence to paginate.
59
+ cursor: Optional cursor from a previous request. None for first page.
60
+ page_size: Maximum number of items per page.
61
+
62
+ Returns:
63
+ Tuple of (page_items, next_cursor). next_cursor is None if no more pages.
64
+
65
+ Raises:
66
+ ValueError: If the cursor is invalid.
67
+ """
68
+ offset = 0
69
+ if cursor:
70
+ state = CursorState.decode(cursor)
71
+ offset = state.offset
72
+
73
+ end = offset + page_size
74
+ page = list(items[offset:end])
75
+
76
+ next_cursor = None
77
+ if end < len(items):
78
+ next_cursor = CursorState(offset=end).encode()
79
+
80
+ return page, next_cursor