From 652c4796a953508a4ba113d111bcce6eeb08fec6 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 2 Feb 2026 02:35:43 -0700 Subject: [PATCH] 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). --- src/gnuradio_mcp/providers/mcp_runtime.py | 90 ++++++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/src/gnuradio_mcp/providers/mcp_runtime.py b/src/gnuradio_mcp/providers/mcp_runtime.py index 7383b19..fd026d5 100644 --- a/src/gnuradio_mcp/providers/mcp_runtime.py +++ b/src/gnuradio_mcp/providers/mcp_runtime.py @@ -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):