@@ -175,6 +175,10 @@ async def _lifespan_manager(self: FastMCP) -> AsyncIterator[None]:
175175 for provider in self .providers :
176176 await stack .enter_async_context (provider .lifespan ())
177177
178+ # After providers are up, adjust MCP handlers to reflect actual
179+ # backend capabilities (removes handlers for unsupported methods).
180+ self ._sync_proxy_capabilities ()
181+
178182 self ._started .set ()
179183 try :
180184 yield
@@ -191,6 +195,112 @@ async def _lifespan_manager(self: FastMCP) -> AsyncIterator[None]:
191195 self ._lifespan_result_set = False
192196 self ._lifespan_result = None
193197
198+ def _sync_proxy_capabilities (self : FastMCP ) -> None :
199+ """Remove MCP handlers for capabilities the backend does not support.
200+
201+ After provider lifespans have run, any ProxyProvider instances have had a
202+ chance to preload their backend's serverCapabilities. If the backend doesn't
203+ support a capability (resources, prompts, tools) and there are no local
204+ components of that type either, we remove the corresponding request handlers
205+ from the low-level MCP server.
206+
207+ This has two effects:
208+ 1. The ``initialize`` response no longer advertises unsupported capabilities.
209+ 2. Clients that try to use an unsupported method receive a proper
210+ ``METHOD_NOT_FOUND`` (-32601) JSON-RPC error instead of an empty list.
211+
212+ The adjustment is conservative: if there are any providers whose capabilities
213+ are not known (i.e. not a LocalProvider or ProxyProvider with loaded caps),
214+ we leave the handlers untouched.
215+ """
216+ import mcp .types
217+
218+ from fastmcp .server .providers .local_provider .local_provider import LocalProvider
219+ from fastmcp .server .providers .proxy import ProxyProvider
220+ from fastmcp .server .providers .wrapped_provider import _WrappedProvider
221+
222+ def _unwrap (p : Any ) -> Any :
223+ """Recursively unwrap _WrappedProvider to reach the inner provider."""
224+ while isinstance (p , _WrappedProvider ):
225+ p = p ._inner
226+ return p
227+
228+ # Restore handlers to the baseline that was saved at construction time
229+ # so that a server reused across multiple lifespan cycles starts clean.
230+ baseline = getattr (self ._mcp_server , "_baseline_request_handlers" , None )
231+ if baseline is not None :
232+ self ._mcp_server .request_handlers = dict (baseline )
233+ else :
234+ self ._mcp_server ._baseline_request_handlers = dict ( # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
235+ self ._mcp_server .request_handlers
236+ )
237+
238+ # Unwrap all providers so we can inspect the actual provider type,
239+ # including namespaced providers wrapped in _WrappedProvider.
240+ unwrapped = [_unwrap (p ) for p in self .providers ]
241+
242+ all_proxy_providers = [p for p in unwrapped if isinstance (p , ProxyProvider )]
243+ if not all_proxy_providers :
244+ return
245+
246+ # If any ProxyProvider failed to preload capabilities, we can't safely
247+ # prune: that backend's capabilities are unknown and removing handlers
248+ # could break capabilities it can actually serve.
249+ if any (p ._backend_capabilities is None for p in all_proxy_providers ):
250+ return
251+
252+ # Only adjust when every provider is either a LocalProvider or a
253+ # ProxyProvider with known capabilities. Unknown providers may have
254+ # components we can't inspect synchronously, so we leave things alone.
255+ if any (not isinstance (p , (LocalProvider , ProxyProvider )) for p in unwrapped ):
256+ return
257+
258+ # Aggregate: a capability is "supported" if ANY proxy backend supports it.
259+ backend_caps = [
260+ p ._backend_capabilities
261+ for p in all_proxy_providers
262+ if p ._backend_capabilities is not None
263+ ]
264+ any_resources = any (bool (c .resources ) for c in backend_caps )
265+ any_prompts = any (bool (c .prompts ) for c in backend_caps )
266+ any_tools = any (bool (c .tools ) for c in backend_caps )
267+
268+ # Check all LocalProvider instances for statically-registered components.
269+ # A user may pass additional LocalProvider instances via the providers kwarg,
270+ # so we aggregate across every LocalProvider in self.providers, not just
271+ # the server's built-in self._local_provider.
272+ from fastmcp .prompts .base import Prompt
273+ from fastmcp .resources .base import Resource
274+ from fastmcp .resources .template import ResourceTemplate
275+ from fastmcp .tools .base import Tool
276+
277+ local_components = [
278+ c
279+ for p in unwrapped
280+ if isinstance (p , LocalProvider )
281+ for c in p ._components .values ()
282+ ]
283+ local_has_resources = any (
284+ isinstance (c , (Resource , ResourceTemplate )) for c in local_components
285+ )
286+ local_has_prompts = any (isinstance (c , Prompt ) for c in local_components )
287+ local_has_tools = any (isinstance (c , Tool ) for c in local_components )
288+
289+ if not any_resources and not local_has_resources :
290+ self ._mcp_server .request_handlers .pop (mcp .types .ListResourcesRequest , None )
291+ self ._mcp_server .request_handlers .pop (
292+ mcp .types .ListResourceTemplatesRequest , None
293+ )
294+ self ._mcp_server .request_handlers .pop (mcp .types .ReadResourceRequest , None )
295+
296+ if not any_prompts and not local_has_prompts :
297+ self ._mcp_server .request_handlers .pop (mcp .types .ListPromptsRequest , None )
298+ self ._mcp_server .request_handlers .pop (mcp .types .GetPromptRequest , None )
299+
300+ if not any_tools and not local_has_tools :
301+ self ._mcp_server .request_handlers .pop (mcp .types .ListToolsRequest , None )
302+ self ._mcp_server .request_handlers .pop (mcp .types .CallToolRequest , None )
303+
194304 def _setup_task_protocol_handlers (self : FastMCP ) -> None :
195305 """Register SEP-1686 task protocol handlers with SDK.
196306
0 commit comments