[PATCH 01/23] rteval: Add prototype MCP server for result analysis

John Kacur <[email protected]>
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add a Model Context Protocol (MCP) server to provide tools for querying
and analyzing rteval test results. This is a first prototype focused on
multi-run analysis rather than single-result querying.

The MCP server provides three initial tools:
- list_results: Discover rteval XML files in a directory
- parse_result: Extract key metrics from rteval output
- compare_results: Compare two result files (placeholder)

The parser correctly handles rteval 3.11 XML format including:
- Run information (duration, timestamp)
- System information (OS, kernel, architecture)
- Timerlat and cyclictest measurement statistics

Target users are QE engineers and developers who run rteval regularly
for RT kernel validation, performance regression testing, and CI/CD.

Tested with rteval 3.11 output on Fedora 43.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 mcp-server/DEPENDENCIES.md |  25 ++++
 mcp-server/README.md       |  40 ++++++
 mcp-server/server.py       | 256 +++++++++++++++++++++++++++++++++++++
 3 files changed, 321 insertions(+)
 create mode 100644 mcp-server/DEPENDENCIES.md
 create mode 100644 mcp-server/README.md
 create mode 100755 mcp-server/server.py

diff --git a/mcp-server/DEPENDENCIES.md b/mcp-server/DEPENDENCIES.md
new file mode 100644
index 000000000000..e139bd7df718
--- /dev/null
+++ b/mcp-server/DEPENDENCIES.md
@@ -0,0 +1,25 @@
+# Dependencies
+
+## Fedora Packages
+
+Install these packages on Fedora:
+
+```bash
+sudo dnf install python3-mcp python3-mcp+cli
+```
+
+- **python3-mcp** - Base MCP SDK for building the server
+- **python3-mcp+cli** - CLI tools for development and testing
+
+## Python Dependencies
+
+For parsing rteval results:
+- **lxml** - XML parsing (should already be available on Fedora as `python3-lxml`)
+
+```bash
+sudo dnf install python3-lxml
+```
+
+## Optional
+
+- **python3-mcp+rich** - For enhanced terminal output (optional)
diff --git a/mcp-server/README.md b/mcp-server/README.md
new file mode 100644
index 000000000000..c5f6936b40d2
--- /dev/null
+++ b/mcp-server/README.md
@@ -0,0 +1,40 @@
+# rteval MCP Server
+
+An MCP (Model Context Protocol) server for querying and analyzing rteval test results.
+
+## Purpose
+
+This MCP server provides tools to:
+- List and discover rteval result files
+- Parse rteval XML/JSON output
+- Compare multiple test runs
+- Query historical performance data
+- Analyze latency trends across runs
+
+## Target Users
+
+System administrators, QE engineers, and developers who run rteval regularly for:
+- Real-time kernel validation
+- Performance regression testing
+- CI/CD pipelines
+- System certification
+
+## Current Status
+
+**Prototype/Development** - This is a first attempt at building an MCP server for rteval.
+The initial focus is on multi-run analysis rather than single-result querying.
+
+## Installation
+
+```bash
+pip install -r requirements.txt
+```
+
+## Usage
+
+TBD - Server configuration and client usage instructions to be added.
+
+## Development
+
+This is a work in progress. The MCP server is being developed in a branch of the
+main rteval repository and may be split into a separate repository later.
diff --git a/mcp-server/server.py b/mcp-server/server.py
new file mode 100755
index 000000000000..a3ca9e3c5783
--- /dev/null
+++ b/mcp-server/server.py
@@ -0,0 +1,256 @@
+#!/usr/bin/env python3
+"""
+rteval MCP Server
+
+An MCP server for querying and analyzing rteval test results.
+Provides tools to list, parse, and compare rteval XML result files.
+"""
+
+import os
+import sys
+from pathlib import Path
+from typing import Any
+from xml.etree import ElementTree as ET
+
+from mcp.server import Server
+from mcp.server.stdio import stdio_server
+from mcp.types import Tool, TextContent
+
+
+# Initialize the MCP server
+app = Server("rteval-mcp-server")
+
+
[email protected]_tools()
+async def list_tools() -> list[Tool]:
+    """List available tools for rteval analysis."""
+    return [
+        Tool(
+            name="list_results",
+            description="List rteval result files in a directory",
+            inputSchema={
+                "type": "object",
+                "properties": {
+                    "directory": {
+                        "type": "string",
+                        "description": "Directory to search for rteval results (default: current directory)",
+                    },
+                    "pattern": {
+                        "type": "string",
+                        "description": "File pattern to match (default: *.xml)",
+                        "default": "*.xml",
+                    },
+                },
+                "required": [],
+            },
+        ),
+        Tool(
+            name="parse_result",
+            description="Parse an rteval XML result file and extract key metrics",
+            inputSchema={
+                "type": "object",
+                "properties": {
+                    "file_path": {
+                        "type": "string",
+                        "description": "Path to the rteval XML result file",
+                    },
+                },
+                "required": ["file_path"],
+            },
+        ),
+        Tool(
+            name="compare_results",
+            description="Compare two rteval result files",
+            inputSchema={
+                "type": "object",
+                "properties": {
+                    "file1": {
+                        "type": "string",
+                        "description": "Path to the first rteval XML result file",
+                    },
+                    "file2": {
+                        "type": "string",
+                        "description": "Path to the second rteval XML result file",
+                    },
+                },
+                "required": ["file1", "file2"],
+            },
+        ),
+    ]
+
+
[email protected]_tool()
+async def call_tool(name: str, arguments: Any) -> list[TextContent]:
+    """Handle tool calls."""
+
+    if name == "list_results":
+        directory = arguments.get("directory", ".")
+        pattern = arguments.get("pattern", "*.xml")
+
+        try:
+            path = Path(directory)
+            if not path.exists():
+                return [TextContent(
+                    type="text",
+                    text=f"Error: Directory '{directory}' does not exist"
+                )]
+
+            # Find matching files
+            files = list(path.glob(pattern))
+
+            if not files:
+                return [TextContent(
+                    type="text",
+                    text=f"No files matching '{pattern}' found in '{directory}'"
+                )]
+
+            # Sort by modification time (newest first)
+            files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
+
+            result = f"Found {len(files)} rteval result file(s) in '{directory}':\n\n"
+            for f in files:
+                mtime = f.stat().st_mtime
+                size = f.stat().st_size
+                result += f"  {f.name}\n"
+                result += f"    Path: {f.absolute()}\n"
+                result += f"    Size: {size:,} bytes\n"
+                result += f"    Modified: {Path(f).stat().st_mtime}\n\n"
+
+            return [TextContent(type="text", text=result)]
+
+        except Exception as e:
+            return [TextContent(
+                type="text",
+                text=f"Error listing results: {str(e)}"
+            )]
+
+    elif name == "parse_result":
+        file_path = arguments["file_path"]
+
+        try:
+            path = Path(file_path)
+            if not path.exists():
+                return [TextContent(
+                    type="text",
+                    text=f"Error: File '{file_path}' does not exist"
+                )]
+
+            # Parse XML
+            tree = ET.parse(file_path)
+            root = tree.getroot()
+
+            # Extract basic information
+            result = f"rteval Result Summary for: {path.name}\n"
+            result += "=" * 60 + "\n\n"
+
+            # Extract rteval version
+            rteval_version = root.get("version", "unknown")
+            result += f"rteval version: {rteval_version}\n\n"
+
+            # Run information
+            runinfo = root.find("run_info")
+            if runinfo is not None:
+                result += "Run Information:\n"
+                result += f"  Duration: {runinfo.get('days', '0')} days, "
+                result += f"{runinfo.get('hours', '0')} hours, "
+                result += f"{runinfo.get('minutes', '0')} minutes, "
+                result += f"{runinfo.get('seconds', '0')} seconds\n"
+
+                date_elem = runinfo.find("date")
+                time_elem = runinfo.find("time")
+                if date_elem is not None and time_elem is not None:
+                    result += f"  Started: {date_elem.text} {time_elem.text}\n"
+                result += "\n"
+
+            # System information
+            sysinfo = root.find("SystemInfo")
+            if sysinfo is not None:
+                uname = sysinfo.find("uname")
+                if uname is not None:
+                    result += "System Information:\n"
+                    for elem in ["baseos", "node", "arch", "kernel"]:
+                        node = uname.find(elem)
+                        if node is not None:
+                            if elem == "kernel":
+                                is_rt = node.get("is_RT", "0")
+                                result += f"  {elem}: {node.text} (RT: {'Yes' if is_rt == '1' else 'No'})\n"
+                            else:
+                                result += f"  {elem}: {node.text}\n"
+                    result += "\n"
+
+            # Timerlat measurements
+            timerlat = root.find(".//timerlat")
+            if timerlat is not None:
+                result += "Timerlat Measurements:\n"
+                cmd_line = timerlat.get("command_line", "")
+                result += f"  Command: {cmd_line}\n\n"
+
+                stats = timerlat.find(".//statistics")
+                if stats is not None:
+                    result += "  Statistics:\n"
+                    for stat in stats:
+                        unit = stat.get("unit", "")
+                        unit_str = f" {unit}" if unit else ""
+                        result += f"    {stat.tag}: {stat.text}{unit_str}\n"
+                result += "\n"
+
+            # Cyclictest measurements (if present instead of timerlat)
+            cyclictest = root.find(".//cyclictest")
+            if cyclictest is not None:
+                result += "Cyclictest Measurements:\n"
+                cmd_line = cyclictest.get("command_line", "")
+                result += f"  Command: {cmd_line}\n\n"
+
+                stats = cyclictest.find(".//statistics")
+                if stats is not None:
+                    result += "  Statistics:\n"
+                    for stat in stats:
+                        unit = stat.get("unit", "")
+                        unit_str = f" {unit}" if unit else ""
+                        result += f"    {stat.tag}: {stat.text}{unit_str}\n"
+                result += "\n"
+
+            return [TextContent(type="text", text=result)]
+
+        except ET.ParseError as e:
+            return [TextContent(
+                type="text",
+                text=f"Error parsing XML file: {str(e)}"
+            )]
+        except Exception as e:
+            return [TextContent(
+                type="text",
+                text=f"Error reading result file: {str(e)}"
+            )]
+
+    elif name == "compare_results":
+        file1 = arguments["file1"]
+        file2 = arguments["file2"]
+
+        # For now, just return a placeholder
+        # This would need actual comparison logic based on rteval structure
+        return [TextContent(
+            type="text",
+            text=f"Comparison tool not yet implemented.\nWould compare:\n  {file1}\n  {file2}\n"
+        )]
+
+    else:
+        return [TextContent(
+            type="text",
+            text=f"Unknown tool: {name}"
+        )]
+
+
+async def main():
+    """Run the MCP server."""
+    async with stdio_server() as (read_stream, write_stream):
+        await app.run(
+            read_stream,
+            write_stream,
+            app.create_initialization_options(),
+        )
+
+
+if __name__ == "__main__":
+    import asyncio
+    asyncio.run(main())
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.