feat: add MCP client debug tools

Add get_client_capabilities and list_client_roots tools to inspect
MCP client information. These help debug MCP connections by exposing:
- Client name/version (e.g., "claude-code" v2.1.15)
- Protocol version and supported capabilities
- Root directories advertised by the client (typically CWD)

Both tools are always registered (part of mode control, not runtime).
This commit is contained in:
Ryan Malloy 2026-02-02 02:35:43 -07:00
parent 3811099623
commit 652c4796a9

View File

@ -1,9 +1,9 @@
from __future__ import annotations
import logging
from typing import Callable
from typing import Any, Callable
from fastmcp import FastMCP
from fastmcp import Context, FastMCP
from pydantic import BaseModel
from gnuradio_mcp.middlewares.docker import DockerMiddleware
@ -22,6 +22,25 @@ class RuntimeModeStatus(BaseModel):
oot_available: bool
class ClientCapabilities(BaseModel):
"""MCP client capability information from initialize handshake."""
client_name: str | None = None
client_version: str | None = None
protocol_version: str | None = None
capabilities: dict[str, Any] = {}
roots_supported: bool = False
sampling_supported: bool = False
experimental: dict[str, Any] = {}
class ClientRoot(BaseModel):
"""A root directory advertised by the MCP client."""
uri: str
name: str | None = None
class McpRuntimeProvider:
"""Registers runtime control tools with FastMCP.
@ -130,8 +149,73 @@ class McpRuntimeProvider:
oot_available=self._provider._has_oot,
)
# Debug tools for MCP client inspection
@self._mcp.tool
async def get_client_capabilities(ctx: Context) -> ClientCapabilities:
"""Get the connected MCP client's capabilities.
Returns information about the client including:
- Client name and version (e.g., "claude-code" v2.1.15)
- MCP protocol version
- Supported capabilities (roots, sampling, etc.)
- Experimental features
Useful for debugging MCP connections and understanding
what features the client supports.
"""
session = ctx.session
client_params = session.client_params if session else None
if client_params is None:
return ClientCapabilities()
client_info = getattr(client_params, "clientInfo", None)
caps = getattr(client_params, "capabilities", None)
result = ClientCapabilities(
client_name=client_info.name if client_info else None,
client_version=client_info.version if client_info else None,
protocol_version=getattr(client_params, "protocolVersion", None),
)
if caps:
if hasattr(caps, "roots") and caps.roots is not None:
result.roots_supported = True
result.capabilities["roots"] = {
"listChanged": getattr(caps.roots, "listChanged", None)
}
if hasattr(caps, "sampling") and caps.sampling is not None:
result.sampling_supported = True
result.capabilities["sampling"] = {}
if hasattr(caps, "experimental"):
result.experimental = caps.experimental or {}
return result
@self._mcp.tool
async def list_client_roots(ctx: Context) -> list[ClientRoot]:
"""List the root directories advertised by the MCP client.
Roots represent project directories or workspaces the client
wants the server to be aware of. Typically includes the
current working directory.
Returns empty list if roots capability is not supported.
"""
try:
roots = await ctx.list_roots()
return [
ClientRoot(uri=str(root.uri), name=root.name)
for root in roots
]
except Exception as e:
logger.warning("Failed to list client roots: %s", e)
return []
logger.info(
"Registered 3 mode control tools (runtime mode disabled by default)"
"Registered 5 mode control tools (runtime mode disabled by default)"
)
def _register_runtime_tools(self):