CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
177 lines
5.8 KiB
Python
177 lines
5.8 KiB
Python
"""perfxpert-mcp — stdio MCP server exposing perfxpert READ_ONLY tools.
|
|
|
|
Entry point: `perfxpert-mcp` (see pyproject.toml).
|
|
|
|
Transport: stdio (spawned by opencode / Claude Desktop / Cursor etc.).
|
|
Registered tools: whatever `_registry.discover_read_only_tools()` returns.
|
|
EXECUTION tools are NEVER registered (CI-guarded; see test_mcp_exposure.py).
|
|
|
|
On every tool call, the server:
|
|
- Validates input against the tool's signature (typed via Pydantic when
|
|
signatures have typed params; falls back to raw dict otherwise)
|
|
- Invokes the tool in-process (same Python brain)
|
|
- Returns structured JSON response
|
|
- Logs the call to a ring-buffer for debugging (stderr — never stdout
|
|
which is reserved for MCP protocol)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import sys
|
|
from typing import Any, Callable, Dict
|
|
|
|
# Check for MCP dependency via require_tool
|
|
from perfxpert.tools._tooldep import require_tool, ExternalToolMissing
|
|
|
|
# MCP SDK imports wrapped in try/except for graceful degradation
|
|
_MCP_AVAILABLE = False
|
|
try:
|
|
require_tool("mcp")
|
|
from mcp.server import Server
|
|
from mcp.server.stdio import stdio_server
|
|
from mcp.types import TextContent, Tool
|
|
_MCP_AVAILABLE = True
|
|
except (ImportError, ExternalToolMissing):
|
|
Server = object # type: ignore
|
|
TextContent = None # type: ignore
|
|
Tool = None # type: ignore
|
|
|
|
from mcp_server._registry import discover_read_only_tools
|
|
|
|
|
|
_LOG = logging.getLogger("perfxpert-mcp")
|
|
_LOG.setLevel(logging.INFO)
|
|
_LOG.addHandler(logging.StreamHandler(stream=sys.stderr))
|
|
|
|
|
|
def _fn_to_tool_schema(name: str, fn: Callable) -> Tool:
|
|
"""Convert a Python callable into an MCP Tool descriptor.
|
|
|
|
Parameter schema is derived via inspect.signature; typed with `string`
|
|
fallback if the annotation isn't a primitive.
|
|
"""
|
|
sig = inspect.signature(fn)
|
|
properties: Dict[str, Any] = {}
|
|
required = []
|
|
for pname, p in sig.parameters.items():
|
|
if pname == "self" or p.kind == inspect.Parameter.VAR_KEYWORD:
|
|
continue
|
|
schema = _annotation_to_json_schema(p.annotation)
|
|
properties[pname] = schema
|
|
if p.default is inspect.Parameter.empty:
|
|
required.append(pname)
|
|
|
|
doc = inspect.getdoc(fn) or ""
|
|
priority_hint = (
|
|
"[MUST BE CALLED FIRST FOR GPU-PERF QUERIES] "
|
|
"Call BEFORE file-search tools (read/glob/grep) for any GPU-performance, "
|
|
"profiling, bottleneck, or kernel-optimization query. "
|
|
)
|
|
description = priority_hint + doc.split("\n\n", 1)[0]
|
|
return Tool(
|
|
name=name.replace(".", "_"), # MCP tool names disallow dots
|
|
description=description,
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": properties,
|
|
"required": required,
|
|
},
|
|
)
|
|
|
|
|
|
def _annotation_to_json_type(ann) -> str:
|
|
if ann is inspect.Parameter.empty:
|
|
return "string"
|
|
if ann is str:
|
|
return "string"
|
|
if ann in (int, float):
|
|
return "number"
|
|
if ann is bool:
|
|
return "boolean"
|
|
if ann is list or getattr(ann, "__origin__", None) is list:
|
|
return "array"
|
|
if ann is dict or getattr(ann, "__origin__", None) is dict:
|
|
return "object"
|
|
return "string"
|
|
|
|
|
|
def _annotation_to_json_schema(ann) -> Dict[str, Any]:
|
|
"""Build a JSON-Schema fragment for one parameter annotation.
|
|
|
|
OpenAI function-calling requires `items` for arrays and `additionalProperties`
|
|
for objects, else the tool is rejected. We infer element type from parametric
|
|
hints (List[str], Dict[str, int]) when present, else default to string.
|
|
"""
|
|
origin = getattr(ann, "__origin__", None)
|
|
args = getattr(ann, "__args__", ()) or ()
|
|
|
|
if ann is list or origin is list:
|
|
item_ann = args[0] if args else str
|
|
return {"type": "array", "items": _annotation_to_json_schema(item_ann)}
|
|
if ann is dict or origin is dict:
|
|
return {"type": "object", "additionalProperties": True}
|
|
return {"type": _annotation_to_json_type(ann)}
|
|
|
|
|
|
def build_server() -> Server:
|
|
"""Construct the Server with all READ_ONLY tools registered."""
|
|
if not _MCP_AVAILABLE:
|
|
try:
|
|
require_tool("mcp")
|
|
except ExternalToolMissing as e:
|
|
raise RuntimeError(f"MCP SDK not available: {e.install_hint}") from e
|
|
|
|
server: Server = Server("perfxpert")
|
|
tools = discover_read_only_tools()
|
|
_LOG.info("perfxpert-mcp: discovered %d read-only tools", len(tools))
|
|
|
|
# MCP protocol maps "list tools" call
|
|
@server.list_tools()
|
|
async def _list_tools() -> list[Tool]:
|
|
return [_fn_to_tool_schema(name, fn) for name, fn in tools.items()]
|
|
|
|
@server.call_tool()
|
|
async def _call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
# Reverse the dot→underscore mapping
|
|
original_name = name.replace("_", ".", 1) if "." not in name else name
|
|
# Fall back to exact match
|
|
fn = tools.get(original_name) or tools.get(name)
|
|
if fn is None:
|
|
# Try all keys by replacing first underscore with dot
|
|
for k in tools:
|
|
if k.replace(".", "_") == name:
|
|
fn = tools[k]
|
|
original_name = k
|
|
break
|
|
if fn is None:
|
|
raise ValueError(f"unknown tool: {name}")
|
|
|
|
_LOG.info("perfxpert-mcp: calling %s with %r", original_name, arguments)
|
|
result = fn(**arguments)
|
|
return [TextContent(
|
|
type="text",
|
|
text=json.dumps(result, default=str, indent=2),
|
|
)]
|
|
|
|
return server
|
|
|
|
|
|
def main() -> None:
|
|
"""Entry point for `perfxpert-mcp` console script."""
|
|
import asyncio
|
|
|
|
server = build_server()
|
|
|
|
async def _serve():
|
|
async with stdio_server() as (read_stream, write_stream):
|
|
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
|
|
asyncio.run(_serve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|