MCP服务器基准测试指南

提供MCP服务器基准测试的完整流程与工具链,涵盖环境搭建、场景设计、能耗测量与数据分析,用于绿色计算领域的能效优化研究。

  • by MdAhbab
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 MdAhbab/MCP_Server_Benchmarks

使用说明

MCP Server Benchmarking Guide for Green Computing Research

Overview

This guide provides detailed instructions for conducting benchmarking experiments on MCP (Model Context Protocol) servers to generate empirical data for the study of energy-efficient MCP architectures.


Table of Contents

  1. Environment Setup
  2. Benchmark Scenarios
  3. Measurement Tools
  4. Detailed Benchmarking Procedures
  5. Data Collection Templates
  6. Analysis Methods
  7. Paper Integration Guidelines

1. Environment Setup

1.1 Hardware Requirements Documentation

Before benchmarking, document your system specifications for reproducibility:

# Get system information
systeminfo | Select-String "OS Name|OS Version|System Type|Total Physical Memory|Processor"

# Get CPU details
Get-WmiObject -Class Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed

# Get RAM details
Get-WmiObject -Class Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum

# Get disk type (SSD/HDD)
Get-PhysicalDisk | Select-Object MediaType, Size, FriendlyName

1.2 Software Prerequisites

Install Node.js (for JavaScript MCP servers)

# Using winget
winget install OpenJS.NodeJS.LTS

# Verify installation
node --version
npm --version

Install Python (for Python MCP servers)

# Using winget
winget install Python.Python.3.12

# Verify installation
python --version
pip --version

Install Docker (for containerized benchmarks)

# Download Docker Desktop from https://www.docker.com/products/docker-desktop/
# Or using winget
winget install Docker.DockerDesktop

# Verify installation
docker --version

Install UV (Fast Python package manager for MCP)

# Install UV
pip install uv

# Or using PowerShell
irm https://astral.sh/uv/install.ps1 | iex

1.3 MCP SDK Installation

# Create benchmark workspace
mkdir C:\MCP_Benchmarks
cd C:\MCP_Benchmarks

# Install MCP SDK for Python
pip install mcp

# Install MCP SDK for TypeScript/JavaScript
npm init -y
npm install @modelcontextprotocol/sdk

1.4 Install Claude Desktop (MCP Host)

Download from: https://claude.ai/download

Configure MCP servers in: %APPDATA%\Claude\claude_desktop_config.json


2. Benchmark Scenarios

Key scenarios to benchmark:

Scenario 1: Token Consumption Comparison

Goal: Measure token usage in traditional ReAct vs Progressive Disclosure patterns

Scenario 2: Transport Mechanism Latency

Goal: Compare stdio vs HTTP vs SSE transport latencies

Scenario 3: Serialization Overhead

Goal: Measure JSON-RPC serialization costs vs alternatives

Scenario 4: Caching Effectiveness

Goal: Quantify performance improvements from caching strategies

Scenario 5: Resource Utilization

Goal: Measure CPU, memory, and energy consumption across server types

Scenario 6: Multi-Server Scalability

Goal: Test performance degradation with increasing server count


3. Measurement Tools

3.1 Energy Measurement Tools

Windows Built-in Power Monitoring

# Enable energy estimation (requires admin)
powercfg /energy /duration 60

# View power usage
powercfg /batteryreport

Intel Power Gadget (for Intel CPUs)

Download: https://www.intel.com/content/www/us/en/developer/articles/tool/power-gadget.html

HWiNFO64 (Comprehensive hardware monitoring)

Download: https://www.hwinfo.com/download/

Open Hardware Monitor (CPU/GPU power)

# Install via Chocolatey
choco install openhardwaremonitor

3.2 Performance Monitoring Tools

Custom PowerShell Monitoring Script

# Save as: C:\MCP_Benchmarks\monitor.ps1

param(
    [int]$DurationSeconds = 60,
    [int]$IntervalMs = 1000,
    [string]$OutputFile = "metrics.csv"
)

$metrics = @()
$endTime = (Get-Date).AddSeconds($DurationSeconds)

while ((Get-Date) -lt $endTime) {
    $cpu = Get-Counter '\Processor(_Total)\% Processor Time' -ErrorAction SilentlyContinue
    $mem = Get-Counter '\Memory\Available MBytes' -ErrorAction SilentlyContinue
    $disk = Get-Counter '\PhysicalDisk(_Total)\Disk Bytes/sec' -ErrorAction SilentlyContinue
    
    $metrics += [PSCustomObject]@{
        Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
        CPU_Percent = [math]::Round($cpu.CounterSamples[0].CookedValue, 2)
        Available_Memory_MB = [math]::Round($mem.CounterSamples[0].CookedValue, 2)
        Disk_Bytes_Sec = [math]::Round($disk.CounterSamples[0].CookedValue, 2)
    }
    
    Start-Sleep -Milliseconds $IntervalMs
}

$metrics | Export-Csv -Path $OutputFile -NoTypeInformation
Write-Host "Metrics saved to $OutputFile"

3.3 Network Monitoring

# Install Wireshark for detailed packet analysis
winget install WiresharkFoundation.Wireshark

# Or use built-in netstat for connection monitoring
netstat -an | findstr "ESTABLISHED"

3.4 Process-Specific Monitoring

# Save as: C:\MCP_Benchmarks\process_monitor.ps1

param(
    [string]$ProcessName,
    [int]$DurationSeconds = 60,
    [string]$OutputFile = "process_metrics.csv"
)

$metrics = @()
$endTime = (Get-Date).AddSeconds($DurationSeconds)

while ((Get-Date) -lt $endTime) {
    $process = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
    
    if ($process) {
        $metrics += [PSCustomObject]@{
            Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
            CPU_Time_Seconds = $process.CPU
            Working_Set_MB = [math]::Round($process.WorkingSet64 / 1MB, 2)
            Private_Memory_MB = [math]::Round($process.PrivateMemorySize64 / 1MB, 2)
            Thread_Count = $process.Threads.Count
            Handle_Count = $process.HandleCount
        }
    }
    
    Start-Sleep -Milliseconds 500
}

$metrics | Export-Csv -Path $OutputFile -NoTypeInformation

4. Detailed Benchmarking Procedures

4.1 Benchmark 1: Token Consumption Analysis

Setup: Create Test MCP Servers

Traditional Server (Verbose Response):

# Save as: C:\MCP_Benchmarks\servers\verbose_server.py

from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
import asyncio
import json

server = Server("verbose-server")

# Simulate large dataset
SAMPLE_DATA = [
    {"id": i, "name": f"Item_{i}", "description": f"Description for item {i} " * 10, 
     "metadata": {"created": "2025-01-01", "modified": "2025-01-05", "tags": ["tag1", "tag2", "tag3"]}}
    for i in range(100)
]

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="get_all_data",
            description="Returns ALL data items with full details",
            inputSchema={"type": "object", "properties": {}}
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "get_all_data":
        # Return everything (inefficient - high token count)
        return [TextContent(type="text", text=json.dumps(SAMPLE_DATA, indent=2))]

async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    asyncio.run(main())

Optimized Server (Progressive Disclosure):

# Save as: C:\MCP_Benchmarks\servers\optimized_server.py

from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
import asyncio
import json

server = Server("optimized-server")

# Same dataset
SAMPLE_DATA = [
    {"id": i, "name": f"Item_{i}", "description": f"Description for item {i} " * 10,
     "metadata": {"created": "2025-01-01", "modified": "2025-01-05", "tags": ["tag1", "tag2", "tag3"]}}
    for i in range(100)
]

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="get_summary",
            description="Returns summary statistics only",
            inputSchema={"type": "object", "properties": {}}
        ),
        Tool(
            name="get_item",
            description="Returns a specific item by ID",
            inputSchema={
                "type": "object",
                "properties": {"id": {"type": "integer"}},
                "required": ["id"]
            }
        ),
        Tool(
            name="search_items",
            description="Search items with filters, returns IDs only",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "limit": {"type": "integer", "default": 10}
                }
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "get_summary":
        # Return only summary (efficient - low token count)
        summary = {
            "total_items": len(SAMPLE_DATA),
            "id_range": f"{SAMPLE_DATA[0]['id']}-{SAMPLE_DATA[-1]['id']}",
            "sample_names": [d["name"] for d in SAMPLE_DATA[:3]]
        }
        return [TextContent(type="text", text=json.dumps(summary))]
    
    elif name == "get_item":
        item_id = arguments.get("id", 0)
        item = next((d for d in SAMPLE_DATA if d["id"] == item_id), None)
        return [TextContent(type="text", text=json.dumps(item) if item else "Not found")]
    
    elif name == "search_items":
        limit = arguments.get("limit", 10)
        # Return only IDs
        ids = [d["id"] for d in SAMPLE_DATA[:limit]]
        return [TextContent(type="text", text=json.dumps({"matching_ids": ids}))]

async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    asyncio.run(main())

Token Counting Script

# Save as: C:\MCP_Benchmarks\token_counter.py

import tiktoken
import json

def count_tokens(text: str, model: str = "cl100k_base") -> int:
    """Count tokens using tiktoken (GPT-4/Claude approximation)"""
    encoding = tiktoken.get_encoding(model)
    return len(encoding.encode(text))

def analyze_response(response_file: str):
    with open(response_file, 'r') as f:
        data = json.load(f)
    
    text = json.dumps(data)
    tokens = count_tokens(text)
    
    print(f"Response size: {len(text)} characters")
    print(f"Token count: {tokens}")
    print(f"Estimated cost (at $0.01/1K tokens): ${tokens * 0.01 / 1000:.4f}")
    
    return tokens

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        analyze_response(sys.argv[1])

Run Token Benchmark

# Install tiktoken
pip install tiktoken

# Test verbose server response
python -c "
import json
data = [{'id': i, 'name': f'Item_{i}', 'description': f'Description for item {i} ' * 10} for i in range(100)]
with open('verbose_response.json', 'w') as f:
    json.dump(data, f, indent=2)
"

# Test optimized server response
python -c "
import json
summary = {'total_items': 100, 'id_range': '0-99', 'sample_names': ['Item_0', 'Item_1', 'Item_2']}
with open('optimized_response.json', 'w') as f:
    json.dump(summary, f)
"

# Count tokens
python token_counter.py verbose_response.json
python token_counter.py optimized_response.json

4.2 Benchmark 2: Transport Latency Comparison

STDIO Transport Server

# Save as: C:\MCP_Benchmarks\servers\stdio_latency_server.py

from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
import asyncio
import time

server = Server("stdio-latency-test")

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="ping",
            description="Simple ping for latency measurement",
            inputSchema={"type": "object", "properties": {"timestamp": {"type": "number"}}}
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "ping":
        client_time = arguments.get("timestamp", 0)
        server_time = time.time() * 1000
        return [TextContent(type="text", text=f'{{"server_time": {server_time}, "client_time": {client_time}}}')]

async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    asyncio.run(main())

HTTP Transport Server

# Save as: C:\MCP_Benchmarks\servers\http_latency_server.py

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import time
import uvicorn

app = FastAPI()

@app.post("/mcp/tools/ping")
async def ping(request: Request):
    data = await request.json()
    client_time = data.get("timestamp", 0)
    server_time = time.time() * 1000
    
    return JSONResponse({
        "result": {
            "server_time": server_time,
            "client_time": client_time,
            "processing_time_ms": server_time - client_time
        }
    })

@app.get("/mcp/tools")
async def list_tools():
    return {"tools": [{"name": "ping", "description": "Latency test"}]}

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8080)

Latency Benchmark Client

# Save as: C:\MCP_Benchmarks\latency_benchmark.py

import asyncio
import aiohttp
import subprocess
import time
import statistics
import json

async def benchmark_http(url: str, iterations: int = 100):
    """Benchmark HTTP transport latency"""
    latencies = []
    
    async with aiohttp.ClientSession() as session:
        for i in range(iterations):
            start = time.perf_counter()
            async with session.post(
                f"{url}/mcp/tools/ping",
                json={"timestamp": time.time() * 1000}
            ) as response:
                await response.json()
            end = time.perf_counter()
            latencies.append((end - start) * 1000)  # Convert to ms
    
    return latencies

def benchmark_stdio(server_script: str, iterations: int = 100):
    """Benchmark STDIO transport latency"""
    latencies = []
    
    # Start server process
    process = subprocess.Popen(
        ["python", server_script],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    
    for i in range(iterations):
        start = time.perf_counter()
        
        # Send JSON-RPC request
        request = json.dumps({
            "jsonrpc": "2.0",
            "id": i,
            "method": "tools/call",
            "params": {"name": "ping", "arguments": {"timestamp": time.time() * 1000}}
        }) + "\n"
        
        process.stdin.write(request)
        process.stdin.flush()
        
        # Read response
        response = process.stdout.readline()
        
        end = time.perf_counter()
        latencies.append((end - start) * 1000)
    
    process.terminate()
    return latencies

def analyze_latencies(latencies: list, name: str):
    """Analyze and report latency statistics"""
    print(f"\n=== {name} Latency Analysis ===")
    print(f"Iterations: {len(latencies)}")
    print(f"Mean: {statistics.mean(latencies):.3f} ms")
    print(f"Median: {statistics.median(latencies):.3f} ms")
    print(f"Std Dev: {statistics.stdev(latencies):.3f} ms")
    print(f"Min: {min(latencies):.3f} ms")
    print(f"Max: {max(latencies):.3f} ms")
    print(f"P95: {sorted(latencies)[int(len(latencies) * 0.95)]:.3f} ms")
    print(f"P99: {sorted(latencies)[int(len(latencies) * 0.99)]:.3f} ms")
    
    return {
        "name": name,
        "mean": statistics.mean(latencies),
        "median": statistics.median(latencies),
        "std": statistics.stdev(latencies),
        "min": min(latencies),
        "max": max(latencies),
        "p95": sorted(latencies)[int(len(latencies) * 0.95)],
        "p99": sorted(latencies)[int(len(latencies) * 0.99)]
    }

async def main():
    results = []
    
    # Benchmark HTTP
    print("Starting HTTP benchmark...")
    http_latencies = await benchmark_http("http://127.0.0.1:8080", iterations=100)
    results.append(analyze_latencies(http_latencies, "HTTP Transport"))
    
    # Save results
    with open("latency_results.json", "w") as f:
        json.dump(results, f, indent=2)
    
    print("\nResults saved to latency_results.json")

if __name__ == "__main__":
    asyncio.run(main())

4.3 Benchmark 3: Serialization Overhead

# Save as: C:\MCP_Benchmarks\serialization_benchmark.py

import json
import time
import statistics
import sys

# Optional: Install msgpack and protobuf for comparison
# pip install msgpack protobuf

def benchmark_json_serialization(data: dict, iterations: int = 10000):
    """Benchmark JSON serialization/deserialization"""
    
    # Serialization
    serialize_times = []
    for _ in range(iterations):
        start = time.perf_counter()
        serialized = json.dumps(data)
        serialize_times.append(time.perf_counter() - start)
    
    # Deserialization
    deserialize_times = []
    json_str = json.dumps(data)
    for _ in range(iterations):
        start = time.perf_counter()
        json.loads(json_str)
        deserialize_times.append(time.perf_counter() - start)
    
    return {
        "format": "JSON",
        "size_bytes": len(json_str.encode('utf-8')),
        "serialize_mean_us": statistics.mean(serialize_times) * 1_000_000,
        "deserialize_mean_us": statistics.mean(deserialize_times) * 1_000_000,
    }

def benchmark_msgpack_serialization(data: dict, iterations: int = 10000):
    """Benchmark MessagePack serialization"""
    try:
        import msgpack
    except ImportError:
        return {"format": "MessagePack", "error": "Not installed"}
    
    serialize_times = []
    for _ in range(iterations):
        start = time.perf_counter()
        serialized = msgpack.packb(data)
        serialize_times.append(time.perf_counter() - start)
    
    deserialize_times = []
    packed = msgpack.packb(data)
    for _ in range(iterations):
        start = time.perf_counter()
        msgpack.unpackb(packed)
        deserialize_times.append(time.perf_counter() - start)
    
    return {
        "format": "MessagePack",
        "size_bytes": len(packed),
        "serialize_mean_us": statistics.mean(serialize_times) * 1_000_000,
        "deserialize_mean_us": statistics.mean(deserialize_times) * 1_000_000,
    }

def create_test_payloads():
    """Create test payloads of varying complexity"""
    
    # Small payload (simple tool call)
    small = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {"name": "get_file", "arguments": {"path": "/test.txt"}}
    }
    
    # Medium payload (tool response with data)
    medium = {
        "jsonrpc": "2.0",
        "id": 1,
        "result": {
            "content": [
                {"type": "text", "text": "x" * 1000}  # 1KB of text
            ],
            "metadata": {
                "tokens": 150,
                "model": "claude-3",
                "timestamp": "2025-01-05T12:00:00Z"
            }
        }
    }
    
    # Large payload (tool definitions)
    large = {
        "jsonrpc": "2.0",
        "id": 1,
        "result": {
            "tools": [
                {
                    "name": f"tool_{i}",
                    "description": f"Description for tool {i} " * 20,
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            f"param_{j}": {"type": "string", "description": f"Parameter {j}"}
                            for j in range(10)
                        }
                    }
                }
                for i in range(50)  # 50 tools
            ]
        }
    }
    
    return {"small": small, "medium": medium, "large": large}

def main():
    payloads = create_test_payloads()
    results = []
    
    for size_name, payload in payloads.items():
        print(f"\n=== Benchmarking {size_name.upper()} payload ===")
        
        json_result = benchmark_json_serialization(payload)
        json_result["payload_size"] = size_name
        results.append(json_result)
        print(f"JSON: {json_result['size_bytes']} bytes, "
              f"serialize: {json_result['serialize_mean_us']:.2f}µs, "
              f"deserialize: {json_result['deserialize_mean_us']:.2f}µs")
        
        msgpack_result = benchmark_msgpack_serialization(payload)
        if "error" not in msgpack_result:
            msgpack_result["payload_size"] = size_name
            results.append(msgpack_result)
            print(f"MessagePack: {msgpack_result['size_bytes']} bytes, "
                  f"serialize: {msgpack_result['serialize_mean_us']:.2f}µs, "
                  f"deserialize: {msgpack_result['deserialize_mean_us']:.2f}µs")
            
            # Calculate overhead
            overhead = (json_result['size_bytes'] - msgpack_result['size_bytes']) / msgpack_result['size_bytes'] * 100
            print(f"JSON overhead vs MessagePack: {overhead:.1f}%")
    
    # Save results
    with open("serialization_results.json", "w") as f:
        json.dump(results, f, indent=2)
    
    print("\nResults saved to serialization_results.json")

if __name__ == "__main__":
    main()

4.4 Benchmark 4: Caching Effectiveness

# Save as: C:\MCP_Benchmarks\caching_benchmark.py

import time
import statistics
import json
from functools import lru_cache
import hashlib

# Simulated expensive operation
def expensive_database_query(query: str) -> dict:
    """Simulate expensive database query (100ms)"""
    time.sleep(0.1)  # Simulate latency
    return {
        "query": query,
        "results": [{"id": i, "data": f"result_{i}"} for i in range(10)],
        "timestamp": time.time()
    }

# In-memory cache implementation
class SimpleCache:
    def __init__(self, ttl_seconds: int = 60):
        self._cache = {}
        self._ttl = ttl_seconds
    
    def _hash_key(self, key: str) -> str:
        return hashlib.md5(key.encode()).hexdigest()
    
    def get(self, key: str):
        hashed = self._hash_key(key)
        if hashed in self._cache:
            value, timestamp = self._cache[hashed]
            if time.time() - timestamp < self._ttl:
                return value
            del self._cache[hashed]
        return None
    
    def set(self, key: str, value):
        hashed = self._hash_key(key)
        self._cache[hashed] = (value, time.time())

cache = SimpleCache(ttl_seconds=300)

def cached_query(query: str) -> dict:
    """Query with caching"""
    cached_result = cache.get(query)
    if cached_result:
        return cached_result
    
    result = expensive_database_query(query)
    cache.set(query, result)
    return result

def benchmark_without_cache(queries: list, iterations: int = 5):
    """Benchmark without caching"""
    times = []
    for _ in range(iterations):
        for query in queries:
            start = time.perf_counter()
            expensive_database_query(query)
            times.append(time.perf_counter() - start)
    return times

def benchmark_with_cache(queries: list, iterations: int = 5):
    """Benchmark with caching"""
    cache._cache.clear()  # Clear cache
    times = []
    
    for iteration in range(iterations):
        for query in queries:
            start = time.perf_counter()
            cached_query(query)
            times.append(time.perf_counter() - start)
    
    return times

def main():
    # Test queries (some repeated to test cache hits)
    queries = [
        "SELECT * FROM users WHERE id = 1",
        "SELECT * FROM users WHERE id = 2",
        "SELECT * FROM users WHERE id = 1",  # Repeat
        "SELECT * FROM orders WHERE user_id = 1",
        "SELECT * FROM users WHERE id = 1",  # Repeat
        "SELECT * FROM products WHERE category = 'electronics'",
        "SELECT * FROM orders WHERE user_id = 1",  # Repeat
    ]
    
    print("=== Caching Effectiveness Benchmark ===\n")
    
    # Benchmark without cache
    print("Running without cache...")
    no_cache_times = benchmark_without_cache(queries, iterations=3)
    
    # Benchmark with cache
    print("Running with cache...")
    with_cache_times = benchmark_with_cache(queries, iterations=3)
    
    # Analyze results
    print("\n=== Results ===")
    print(f"\nWithout Cache:")
    print(f"  Total time: {sum(no_cache_times)*1000:.2f} ms")
    print(f"  Mean per query: {statistics.mean(no_cache_times)*1000:.2f} ms")
    
    print(f"\nWith Cache:")
    print(f"  Total time: {sum(with_cache_times)*1000:.2f} ms")
    print(f"  Mean per query: {statistics.mean(with_cache_times)*1000:.2f} ms")
    
    # Calculate improvement
    improvement = (sum(no_cache_times) - sum(with_cache_times)) / sum(no_cache_times) * 100
    speedup = sum(no_cache_times) / sum(with_cache_times)
    
    print(f"\n=== Improvement ===")
    print(f"  Time reduction: {improvement:.1f}%")
    print(f"  Speedup factor: {speedup:.1f}x")
    
    # Cache statistics
    unique_queries = len(set(queries))
    total_queries = len(queries) * 3  # iterations
    cache_hits = total_queries - unique_queries * 3
    hit_rate = cache_hits / total_queries * 100
    
    print(f"\n=== Cache Statistics ===")
    print(f"  Unique queries: {unique_queries}")
    print(f"  Total queries: {total_queries}")
    print(f"  Estimated cache hits: {cache_hits}")
    print(f"  Hit rate: {hit_rate:.1f}%")
    
    # Save results
    results = {
        "without_cache": {
            "total_ms": sum(no_cache_times) * 1000,
            "mean_ms": statistics.mean(no_cache_times) * 1000
        },
        "with_cache": {
            "total_ms": sum(with_cache_times) * 1000,
            "mean_ms": statistics.mean(with_cache_times) * 1000
        },
        "improvement_percent": improvement,
        "speedup_factor": speedup,
        "cache_hit_rate": hit_rate
    }
    
    with open("caching_results.json", "w") as f:
        json.dump(results, f, indent=2)

if __name__ == "__main__":
    main()

4.5 Benchmark 5: Energy Consumption Measurement

# Save as: C:\MCP_Benchmarks\energy_benchmark.py

import subprocess
import time
import json
import os
import statistics

def get_cpu_power_estimate():
    """
    Estimate CPU power using Windows Performance Counters
    Note: For accurate measurements, use Intel Power Gadget or HWiNFO
    """
    try:
        result = subprocess.run(
            ['powershell', '-Command', 
             "(Get-Counter '\\Processor(_Total)\\% Processor Time').CounterSamples[0].CookedValue"],
            capture_output=True, text=True, timeout=5
        )
        cpu_percent = float(result.stdout.strip())
        
        # Rough estimate: Assume TDP of 65W, scale by usage
        # This is a ROUGH estimate - use proper power monitoring tools for accuracy
        tdp_watts = 65  # Adjust based on your CPU
        estimated_power = (cpu_percent / 100) * tdp_watts
        
        return cpu_percent, estimated_power
    except Exception as e:
        return None, None

def run_workload(workload_func, duration_seconds: int = 30):
    """Run a workload and measure power consumption"""
    measurements = []
    
    start_time = time.time()
    end_time = start_time + duration_seconds
    
    # Start workload in background
    import threading
    workload_running = True
    
    def workload_thread():
        while workload_running:
            workload_func()
    
    thread = threading.Thread(target=workload_thread)
    thread.start()
    
    # Collect measurements
    while time.time() < end_time:
        cpu_percent, power_watts = get_cpu_power_estimate()
        if cpu_percent is not None:
            measurements.append({
                "timestamp": time.time() - start_time,
                "cpu_percent": cpu_percent,
                "estimated_power_watts": power_watts
            })
        time.sleep(0.5)
    
    workload_running = False
    thread.join(timeout=5)
    
    return measurements

def idle_workload():
    """Baseline idle workload"""
    time.sleep(0.1)

def light_workload():
    """Light JSON processing workload"""
    data = {"items": [{"id": i, "value": f"item_{i}"} for i in range(100)]}
    for _ in range(100):
        json.dumps(data)
        json.loads(json.dumps(data))
    time.sleep(0.01)

def heavy_workload():
    """Heavy computation workload"""
    # Simulate heavy serialization/computation
    data = {"items": [{"id": i, "value": f"item_{i}" * 100} for i in range(1000)]}
    for _ in range(10):
        serialized = json.dumps(data)
        json.loads(serialized)

def analyze_measurements(measurements: list, name: str):
    """Analyze energy measurements"""
    if not measurements:
        return None
    
    cpu_values = [m["cpu_percent"] for m in measurements]
    power_values = [m["estimated_power_watts"] for m in measurements]
    
    duration = measurements[-1]["timestamp"] - measurements[0]["timestamp"]
    avg_power = statistics.mean(power_values)
    energy_wh = (avg_power * duration) / 3600  # Convert to Watt-hours
    
    return {
        "name": name,
        "duration_seconds": duration,
        "avg_cpu_percent": statistics.mean(cpu_values),
        "avg_power_watts": avg_power,
        "peak_power_watts": max(power_values),
        "energy_wh": energy_wh,
        "energy_joules": avg_power * duration
    }

def main():
    print("=== Energy Consumption Benchmark ===")
    print("Note: For accurate power measurements, use Intel Power Gadget or HWiNFO")
    print("These are rough estimates based on CPU utilization.\n")
    
    results = []
    
    # Baseline (idle)
    print("Measuring baseline (idle)...")
    idle_measurements = run_workload(idle_workload, duration_seconds=15)
    idle_result = analyze_measurements(idle_measurements, "Idle Baseline")
    if idle_result:
        results.append(idle_result)
        print(f"  Avg CPU: {idle_result['avg_cpu_percent']:.1f}%")
        print(f"  Avg Power: {idle_result['avg_power_watts']:.1f}W")
    
    # Light workload
    print("\nMeasuring light workload (simple JSON operations)...")
    light_measurements = run_workload(light_workload, duration_seconds=15)
    light_result = analyze_measurements(light_measurements, "Light Workload")
    if light_result:
        results.append(light_result)
        print(f"  Avg CPU: {light_result['avg_cpu_percent']:.1f}%")
        print(f"  Avg Power: {light_result['avg_power_watts']:.1f}W")
    
    # Heavy workload
    print("\nMeasuring heavy workload (intensive JSON operations)...")
    heavy_measurements = run_workload(heavy_workload, duration_seconds=15)
    heavy_result = analyze_measurements(heavy_measurements, "Heavy Workload")
    if heavy_result:
        results.append(heavy_result)
        print(f"  Avg CPU: {heavy_result['avg_cpu_percent']:.1f}%")
        print(f"  Avg Power: {heavy_result['avg_power_watts']:.1f}W")
    
    # Calculate energy overhead
    if idle_result and heavy_result:
        overhead = heavy_result['avg_power_watts'] - idle_result['avg_power_watts']
        print(f"\n=== Energy Overhead ===")
        print(f"Heavy workload overhead: {overhead:.1f}W above idle")
        print(f"Percentage increase: {(overhead/idle_result['avg_power_watts'])*100:.1f}%")
    
    # Save results
    with open("energy_results.json", "w") as f:
        json.dump(results, f, indent=2)
    
    print("\nResults saved to energy_results.json")
    print("\nFor publication-quality measurements, consider:")
    print("1. Using Intel Power Gadget for actual CPU power readings")
    print("2. Using a hardware power meter (e.g., Kill-A-Watt)")
    print("3. Running longer duration tests (5-10 minutes)")
    print("4. Multiple runs with statistical analysis")

if __name__ == "__main__":
    main()

4.6 Benchmark 6: Multi-Server Scalability

# Save as: C:\MCP_Benchmarks\scalability_benchmark.py

import asyncio
import time
import statistics
import json

class MockMCPServer:
    """Mock MCP server for scalability testing"""
    def __init__(self, server_id: int, num_tools: int = 10):
        self.server_id = server_id
        self.tools = [
            {
                "name": f"server_{server_id}_tool_{i}",
                "description": f"Tool {i} from server {server_id}",
                "inputSchema": {"type": "object", "properties": {}}
            }
            for i in range(num_tools)
        ]
    
    async def initialize(self):
        """Simulate initialization handshake"""
        await asyncio.sleep(0.01)  # Simulate network latency
        return self.tools
    
    async def call_tool(self, tool_name: str, arguments: dict):
        """Simulate tool invocation"""
        await asyncio.sleep(0.005)  # Simulate processing
        return {"result": f"Response from {tool_name}"}

async def benchmark_initialization(num_servers: int):
    """Benchmark initialization time with multiple servers"""
    servers = [MockMCPServer(i) for i in range(num_servers)]
    
    start = time.perf_counter()
    
    # Initialize all servers concurrently
    all_tools = await asyncio.gather(*[s.initialize() for s in servers])
    
    end = time.perf_counter()
    
    total_tools = sum(len(tools) for tools in all_tools)
    
    return {
        "num_servers": num_servers,
        "total_tools": total_tools,
        "init_time_ms": (end - start) * 1000
    }

async def benchmark_tool_selection(num_servers: int, tools_per_server: int = 10):
    """Benchmark time to find and invoke a tool"""
    servers = [MockMCPServer(i, tools_per_server) for i in range(num_servers)]
    
    # Collect all tools
    all_tools = []
    for s in servers:
        tools = await s.initialize()
        for t in tools:
            all_tools.append((s, t))
    
    # Simulate tool selection (linear search - worst case)
    target_tool = f"server_{num_servers-1}_tool_{tools_per_server-1}"
    
    times = []
    for _ in range(100):
        start = time.perf_counter()
        
        # Find tool (linear search)
        for server, tool in all_tools:
            if tool["name"] == target_tool:
                await server.call_tool(tool["name"], {})
                break
        
        end = time.perf_counter()
        times.append((end - start) * 1000)
    
    return {
        "num_servers": num_servers,
        "total_tools": len(all_tools),
        "mean_selection_time_ms": statistics.mean(times),
        "p99_selection_time_ms": sorted(times)[99]
    }

async def benchmark_context_window_usage(num_servers: int, tools_per_server: int = 10):
    """Estimate context window tokens for tool definitions"""
    servers = [MockMCPServer(i, tools_per_server) for i in range(num_servers)]
    
    all_tools = []
    for s in servers:
        tools = await s.initialize()
        all_tools.extend(tools)
    
    # Estimate tokens (rough: 4 chars per token)
    json_str = json.dumps(all_tools)
    estimated_tokens = len(json_str) / 4
    
    return {
        "num_servers": num_servers,
        "total_tools": len(all_tools),
        "json_bytes": len(json_str),
        "estimated_tokens": int(estimated_tokens),
        "context_percentage": (estimated_tokens / 128000) * 100  # Assume 128K context
    }

async def main():
    print("=== MCP Server Scalability Benchmark ===\n")
    
    server_counts = [1, 5, 10, 25, 50, 100]
    
    results = {
        "initialization": [],
        "tool_selection": [],
        "context_usage": []
    }
    
    for num_servers in server_counts:
        print(f"Testing with {num_servers} servers...")
        
        # Initialization benchmark
        init_result = await benchmark_initialization(num_servers)
        results["initialization"].append(init_result)
        
        # Tool selection benchmark
        selection_result = await benchmark_tool_selection(num_servers)
        results["tool_selection"].append(selection_result)
        
        # Context usage estimate
        context_result = await benchmark_context_window_usage(num_servers)
        results["context_usage"].append(context_result)
    
    # Print summary
    print("\n=== Initialization Time ===")
    print(f"{'Servers':<10} {'Tools':<10} {'Time (ms)':<15}")
    for r in results["initialization"]:
        print(f"{r['num_servers']:<10} {r['total_tools']:<10} {r['init_time_ms']:<15.2f}")
    
    print("\n=== Tool Selection Time ===")
    print(f"{'Servers':<10} {'Tools':<10} {'Mean (ms)':<15} {'P99 (ms)':<15}")
    for r in results["tool_selection"]:
        print(f"{r['num_servers']:<10} {r['total_tools']:<10} {r['mean_selection_time_ms']:<15.2f} {r['p99_selection_time_ms']:<15.2f}")
    
    print("\n=== Context Window Usage ===")
    print(f"{'Servers':<10} {'Tools':<10} {'Est. Tokens':<15} {'% of 128K':<15}")
    for r in results["context_usage"]:
        print(f"{r['num_servers']:<10} {r['total_tools']:<10} {r['estimated_tokens']:<15} {r['context_percentage']:<15.1f}")
    
    # Save results
    with open("scalability_results.json", "w") as f:
        json.dump(results, f, indent=2)
    
    print("\nResults saved to scalability_results.json")

if __name__ == "__main__":
    asyncio.run(main())

5. Data Collection Templates

5.1 Benchmark Run Log Template

# MCP Benchmark Run Log

## Run Information
- **Date:** YYYY-MM-DD HH:MM
- **Run ID:** [unique identifier]
- **Operator:** [name]

## System Configuration
- **OS:** Windows 11 Pro 23H2
- **CPU:** [model] @ [speed] GHz, [cores] cores
- **RAM:** [size] GB DDR[version]
- **Storage:** [type] [size]
- **Power Mode:** [balanced/performance]

## Software Versions
- **Python:** 3.12.x
- **Node.js:** 20.x.x
- **MCP SDK:** x.x.x
- **Docker:** x.x.x

## Test Parameters
- **Warmup Iterations:** [number]
- **Test Iterations:** [number]
- **Cooldown Period:** [seconds]

## Results Summary
| Benchmark | Metric | Value | Unit |
|-----------|--------|-------|------|
| Token Consumption | Verbose | | tokens |
| Token Consumption | Optimized | | tokens |
| Latency | STDIO | | ms |
| Latency | HTTP | | ms |
| Serialization | JSON | | µs |
| Caching | Hit Rate | | % |
| Energy | Idle | | W |
| Energy | Load | | W |

## Observations
[Notes about anomalies, issues, or interesting findings]

5.2 Results CSV Template

run_id,timestamp,benchmark_type,metric_name,value,unit,notes
001,2025-01-05T10:00:00,token_consumption,verbose_tokens,150000,tokens,
001,2025-01-05T10:00:00,token_consumption,optimized_tokens,2000,tokens,
001,2025-01-05T10:05:00,latency,stdio_mean,5.2,ms,
001,2025-01-05T10:05:00,latency,http_mean,12.4,ms,

6. Analysis Methods

6.1 Statistical Analysis Script

# Save as: C:\MCP_Benchmarks\analyze_results.py

import json
import statistics
import os
from pathlib import Path

def load_results(results_dir: str = "."):
    """Load all result files"""
    results = {}
    
    for filename in ["latency_results.json", "serialization_results.json", 
                     "caching_results.json", "energy_results.json",
                     "scalability_results.json"]:
        filepath = Path(results_dir) / filename
        if filepath.exists():
            with open(filepath, 'r') as f:
                results[filename.replace("_results.json", "")] = json.load(f)
    
    return results

def generate_latex_table(results: dict):
    """Generate LaTeX table for paper"""
    
    latex = r"""
\begin{table}[h]
\centering
\caption{Empirical Benchmark Results}
\label{tab:benchmarks}
\begin{tabular}{lrrr}
\toprule
\textbf{Metric} & \textbf{Baseline} & \textbf{Optimized} & \textbf{Improvement} \\
\midrule
"""
    
    # Add rows based on available data
    if "caching" in results:
        cache = results["caching"]
        latex += f"Response Time (ms) & {cache['without_cache']['mean_ms']:.2f} & {cache['with_cache']['mean_ms']:.2f} & {cache['improvement_percent']:.1f}\\% \\\\\n"
    
    latex += r"""
\bottomrule
\end{tabular}
\end{table}
"""
    return latex

def main():
    results = load_results()
    
    print("=== Benchmark Analysis ===\n")
    
    for category, data in results.items():
        print(f"\n--- {category.upper()} ---")
        print(json.dumps(data, indent=2))
    
    # Generate LaTeX
    latex = generate_latex_table(results)
    print("\n=== LaTeX Table ===")
    print(latex)
    
    # Save LaTeX
    with open("benchmark_table.tex", "w") as f:
        f.write(latex)

if __name__ == "__main__":
    main()

6.2 Visualization Script

# Save as: C:\MCP_Benchmarks\visualize_results.py

import json
import matplotlib.pyplot as plt
import numpy as np
from pathlib import Path

def plot_scalability(results_file: str = "scalability_results.json"):
    """Generate scalability charts"""
    
    with open(results_file, 'r') as f:
        data = json.load(f)
    
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    
    # Initialization time
    servers = [r['num_servers'] for r in data['initialization']]
    init_times = [r['init_time_ms'] for r in data['initialization']]
    
    axes[0].plot(servers, init_times, 'b-o')
    axes[0].set_xlabel('Number of Servers')
    axes[0].set_ylabel('Initialization Time (ms)')
    axes[0].set_title('MCP Initialization Scalability')
    axes[0].grid(True)
    
    # Tool selection time
    selection_times = [r['mean_selection_time_ms'] for r in data['tool_selection']]
    
    axes[1].plot(servers, selection_times, 'r-o')
    axes[1].set_xlabel('Number of Servers')
    axes[1].set_ylabel('Selection Time (ms)')
    axes[1].set_title('Tool Selection Latency')
    axes[1].grid(True)
    
    # Context usage
    context_pct = [r['context_percentage'] for r in data['context_usage']]
    
    axes[2].bar(range(len(servers)), context_pct)
    axes[2].set_xticks(range(len(servers)))
    axes[2].set_xticklabels(servers)
    axes[2].set_xlabel('Number of Servers')
    axes[2].set_ylabel('Context Window Usage (%)')
    axes[2].set_title('Context Window Consumption')
    axes[2].axhline(y=50, color='r', linestyle='--', label='50% threshold')
    axes[2].legend()
    
    plt.tight_layout()
    plt.savefig('scalability_charts.png', dpi=300)
    plt.savefig('scalability_charts.pdf')
    print("Charts saved to scalability_charts.png/pdf")

def plot_comparison_bar(data: dict, output_file: str = "comparison_chart.png"):
    """Generate comparison bar chart"""
    
    categories = list(data.keys())
    baseline = [data[c]['baseline'] for c in categories]
    optimized = [data[c]['optimized'] for c in categories]
    
    x = np.arange(len(categories))
    width = 0.35
    
    fig, ax = plt.subplots(figsize=(10, 6))
    bars1 = ax.bar(x - width/2, baseline, width, label='Baseline', color='#ff6b6b')
    bars2 = ax.bar(x + width/2, optimized, width, label='Optimized', color='#4ecdc4')
    
    ax.set_ylabel('Value')
    ax.set_title('MCP Optimization Comparison')
    ax.set_xticks(x)
    ax.set_xticklabels(categories)
    ax.legend()
    
    # Add value labels
    for bar in bars1:
        height = bar.get_height()
        ax.annotate(f'{height:.1f}',
                    xy=(bar.get_x() + bar.get_width() / 2, height),
                    xytext=(0, 3),
                    textcoords="offset points",
                    ha='center', va='bottom')
    
    for bar in bars2:
        height = bar.get_height()
        ax.annotate(f'{height:.1f}',
                    xy=(bar.get_x() + bar.get_width() / 2, height),
                    xytext=(0, 3),
                    textcoords="offset points",
                    ha='center', va='bottom')
    
    plt.tight_layout()
    plt.savefig(output_file, dpi=300)
    print(f"Chart saved to {output_file}")

if __name__ == "__main__":
    # Install matplotlib if needed: pip install matplotlib
    
    if Path("scalability_results.json").exists():
        plot_scalability()
    else:
        print("Run scalability_benchmark.py first to generate data")

7. Paper Integration Guidelines

7.1 Adding Benchmarks Section to Your Paper

Add this section after your existing "Comparative Performance Assessment" section:

\section{Empirical Validation}

\subsection{Experimental Setup}

To validate theoretical findings from the literature, we conducted empirical benchmarks on a controlled test environment. Experiments were performed on a system with [CPU model], [RAM] GB RAM, running Windows 11. All tests were repeated [N] times with results averaged.

\subsection{Token Consumption Validation}

We implemented prototype MCP servers following both traditional verbose response patterns and progressive disclosure architectures. Token counts were measured using the tiktoken library with the cl100k\_base encoding.

\begin{table}[h]
\centering
\caption{Token Consumption Comparison}
\label{tab:token_benchmark}
\begin{tabular}{lrr}
\toprule
\textbf{Scenario} & \textbf{Tokens} & \textbf{Reduction} \\
\midrule
Verbose Response (100 items) & X,XXX & -- \\
Progressive Disclosure & XXX & XX.X\% \\
\bottomrule
\end{tabular}
\end{table}

\subsection{Transport Latency Analysis}

Latency measurements comparing STDIO and HTTP transport mechanisms revealed [findings].

\subsection{Caching Effectiveness}

Response caching demonstrated [X]x performance improvement with a [Y]\% cache hit rate for repeated queries, validating recommendations from the optimization literature \cite{catchmetrics2025}.

\subsection{Scalability Observations}

Testing with increasing numbers of MCP servers (1 to 100) revealed [observations about context window consumption and initialization overhead].

7.2 Adding Figures

\begin{figure}[t]
\centering
\includegraphics[width=\columnwidth]{scalability_charts.pdf}
\caption{MCP Server Scalability Benchmarks: (a) Initialization time scales linearly with server count, (b) Tool selection latency increases with tool registry size, (c) Context window consumption becomes critical beyond 50 servers.}
\label{fig:scalability}
\end{figure}

7.3 Updating Abstract and Contributions

Update your abstract to mention empirical validation:

"...We validate theoretical findings through empirical benchmarks demonstrating [key finding 1] and [key finding 2]..."

Add to your contributions:

"...(5) empirical validation of optimization strategies through controlled benchmarks..."

8. Quick Start Commands

Run all benchmarks in sequence:

# Navigate to benchmark directory
cd C:\MCP_Benchmarks

# Install dependencies
pip install tiktoken aiohttp matplotlib

# Run benchmarks
python serialization_benchmark.py
python caching_benchmark.py
python energy_benchmark.py
python scalability_benchmark.py

# Analyze and visualize
python analyze_results.py
python visualize_results.py

9. Advanced Benchmarks (Optional)

9.1 Real MCP Server Testing with Claude Desktop

Configure Claude Desktop to use your test servers:

// %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "verbose-test": {
      "command": "python",
      "args": ["C:\\MCP_Benchmarks\\servers\\verbose_server.py"]
    },
    "optimized-test": {
      "command": "python", 
      "args": ["C:\\MCP_Benchmarks\\servers\\optimized_server.py"]
    }
  }
}

9.2 Docker-Based Solver Benchmarks

# Save as: C:\MCP_Benchmarks\solver\Dockerfile
FROM python:3.12-slim

RUN pip install mcp ortools z3-solver

COPY solver_server.py /app/
WORKDIR /app

CMD ["python", "solver_server.py"]

9.3 Network Capture for Protocol Analysis

# Capture MCP traffic with Wireshark
# Filter: tcp.port == 8080
# Export: Statistics > Protocol Hierarchy

10. Checklist for Paper Submission

  • [ ] Run all benchmarks minimum 3 times
  • [ ] Document system specifications
  • [ ] Calculate statistical significance (p-values if comparing groups)
  • [ ] Generate publication-quality figures (300 DPI, PDF format)
  • [ ] Update paper with empirical validation section
  • [ ] Add benchmark methodology to reproducibility appendix
  • [ ] Cite this benchmarking methodology in your paper
  • [ ] Make benchmark code available (GitHub repository)

Contact and Support

For issues with this benchmarking guide, consult:

  • MCP SDK Documentation: https://modelcontextprotocol.io
  • Python MCP: https://github.com/modelcontextprotocol/python-sdk
  • TypeScript MCP: https://github.com/modelcontextprotocol/typescript-sdk

Peer-Review Response and Revision Log (July 5, 2026)

This section documents the full revision of the paper (main.tex) and this benchmark suite in response to (a) the external peer review in mcp_survey_peer_review.md (written for an IJCAI-style venue) and (b) our own professional review pass targeting the actual submission venue, IEEE Transactions on Sustainable Computing (TSUSC). Every issue below is either fixed in the paper, fixed in this code base, or answered explicitly.

A. Problems Identified and How They Were Fixed

A.1 Problems raised by the external review

# Review finding Resolution
1 "Exponential energy savings" from O(n^2) attention is mathematically wrong (savings are quadratic/super-linear, not exponential). All occurrences of "exponential" removed. The paper now states: self-attention is O(n^2) in context length, so token reductions yield at-least-proportional energy savings, with the attention term shrinking approximately quadratically. Fixed in the abstract, Sections I, V, VI-B, and the Conclusion.
2 Contradictory offloading direction ("offload to the LLM" vs. "from the LLM to solvers"). The sentence "MCP servers explicitly offload large-scale mathematical computations to the LLM" was a typo and is corrected. Section VII now defines offloading once, explicitly: offloading always means moving work off the GPU-bound LLM, in two forms (data-plane offloading via code execution; compute-plane offloading via deterministic solvers), including the task regimes where each applies.
3 Serialization experiments (MessagePack/Pickle) are not spec-compliant since MCP mandates JSON-RPC 2.0. The subsection is retitled "Transport Serialisation Overhead: A What-If Analysis" and states in italics that binary-format results are prospective, not specification-compliant. The table caption says the same. A new paragraph covers compatibility (capability-flag content negotiation with JSON-RPC fallback), security review of binary parsers, observability trade-offs, and an incremental deployment path (binary framing for high-volume resource payloads only, JSON control plane retained). Pickle is explicitly labelled unsafe for untrusted input and not proposed for MCP.
4 Anomalous power result: mixed workload (22.6 W) below idle (26.4 W). Root cause confirmed as a measurement artifact: v1 sampled system-wide CPU, and the idle window was contaminated by background OS activity (the raw log shows a 78.6% CPU spike during "idle"). energy_benchmark.py was rewritten (v2) with process-level attribution: each workload is charged only for the CPU cycles its own process consumes. Re-run results: idle = 0.00 W attributed (baseline), mixed = 0.53 W, JSON = 3.76 W, heavy compute = 3.78 W. Every active workload now sits strictly above idle, as physics requires. The paper explains the pilot artifact and the fix.
5 Opaque energy instrumentation (no sampling rate, calibration, isolation details). The paper now has a dedicated "Experimental Setup and Measurement Card" subsection (Section VI-A) stating: hardware, OS, Python version, the exact power model equations (P_sys = TDP*(0.30 + 0.70u_sys); P_attr = 0.70TDP*u_proc), 500 ms sampling, 30 s windows, warm-up discard, cool-down, and the isolation strategy. It states plainly that these are utilisation-model estimates, not wall-power measurements, and that RAPL/hardware-meter validation is future work. The same methodology block is embedded in the results JSON.
6 Missing baselines/trials/variance; harness effects not addressed. Two-part fix. (1) Honest scoping: none of our experiments has an LLM controller in the loop, so harness effects cannot confound them; this is now stated explicitly, citing the harness-variance paper (arXiv:2605.23950). End-to-end success rates in the paper are clearly labelled as compiled from published third-party benchmarks (MCP-Universe), not our rig. (2) Statistics: caching and scalability benchmarks now use fixed seeds (42) and 5 repetitions with mean +/- SD reported in the paper's tables; serialization already used 5,000 iterations and now surfaces its SDs; token counts are deterministic (bit-identical across runs), which the paper now says explicitly.
7 Impossible "100.0%" reductions in Tables I and VII. Rounding artifact confirmed: raw value was 99.95%, displayed with a one-decimal format. analyze_results.py and token_benchmark.py now print two decimals, and the paper reports 94.74 / 98.93 / 99.46 / 99.89 / 99.95 with the reduction formula stated in Section VI-A: (T_verbose - T_optimised)/T_verbose x 100, two decimal places. The text notes the reduction approaches but never reaches 100%.
8 Editorial artifacts and promotional tone ("AGI nervous system", "fastest, most secure and greenest backbone", boilerplate dates). The manuscript date is current (2026). All hyperbole removed or hedged: no "nervous system", no "greenest backbone", no "shocking"/"disturbing"/"groundbreaking"/"unbreakable", no AGI-ubiquity claims. The future-facing section is retitled "Open Challenges and Future Research Directions" and its speculative parts are explicitly framed as an outlook, not predictions.
9 Missing related work (harness variance, MemAct, memory survey, SWE-Pruner, Claw-Eval). All five suggested works were independently verified to exist on arXiv before citing (an AI-generated review can hallucinate references; these did not). All are now integrated: a positioning paragraph in Section III, methodological use in Section VI-A (harness disclosure, audited reporting), a "Learned Context Policies" subsection in Section VII (MemAct, SWE-Pruner), Claw-Eval in the security section, and the sleeper-memory-poisoning paper (arXiv:2605.15338) in the threat analysis. References grew from 37 to 43 (TSUSC cap: 45).

A.2 Additional problems we found ourselves (not in the external review)

  1. TSUSC does not accept surveys. The journal's own guidelines state: "TSUSC does not accept submissions of survey or review articles." The old title ("...A Survey of Architectures...") was a desk-reject. The paper is reframed as a regular research paper: new title "Energy-Efficient Model Context Protocol Servers: Architectural Taxonomy, Empirical Benchmarks, and Optimisation Strategies for Sustainable Agentic AI", contributions reordered to lead with the reproducible benchmark suite, and every self-reference to "survey" removed.
  2. Miscitation of the tool-fusion result. The "12% latency reduction" claim was cited to MPC-Pipe (a secure multi-party computation paper, unrelated). The correct source is "An LLM-Tool Compiler for Fused Parallel Function Calling" (Singh et al., arXiv:2405.17438; up to 40% cost and 12% latency reduction). Verified and fixed.
  3. Factually wrong batching claim. "Recent versions require JSON-RPC batching" is incorrect: batching was added in the 2025-03-26 MCP revision and removed in the 2025-06-18 revision. The paper now states this history and notes parallelism comes from concurrent tool invocations.
  4. "Semantic caching" mislabel. The caching benchmark implements exact-match caching (MD5 of tool name + canonicalised arguments), not embedding-based semantic caching. The paper and this suite now say "exact-match response caching"; semantic caching remains discussed as a literature technique that would perform at least as well.
  5. Token-count inconsistency across benchmarks. The scalability benchmark estimated tokens as chars/4 while the token benchmark used tiktoken. scalability_benchmark.py now uses tiktoken cl100k_base (with chars/4 fallback), and the paper's Table IV was regenerated (142,202 tokens at 100 servers, previously 156,075 under the heuristic).
  6. Internal inconsistency: "10,000 requests per day" in the paper vs. 480,000 in the results JSON. The paper now states the scenario assumptions once (480,000 requests/day at 100 req/s) next to the numbers derived from them.
  7. Misattribution: "Skywork AI" credited for MCP-Solver. MCP-Solver is by Szeider (arXiv:2501.00539). Removed.
  8. Success-rate misreading. "43.72% success ... more than half of all tool calls fail" conflated task-level and call-level failure. Corrected to task-level phrasing.
  9. Stale figure captions. All captions now match the regenerated figures and current data (energy figure: 4.87 Wh vs 0.06 Wh; caching: 12.4x; dashboard totals updated), and literature-derived illustrative charts (Images/Chart 1-4) are explicitly labelled as illustrative or literature-synthesised in their captions.
  10. Mixing measured and modelled quantities. The reviewer's underlying concern in 3.2 is fully addressed structurally: every energy number in the paper is now tagged as either a measurement (utilisation-model, process-attributed, with SD) or a scenario analysis / extrapolation (the 98.7% payload-reduction scenario), and the production estimate states its assumptions inline.

B. Changes to This Benchmark Suite (code)

  • energy_benchmark.py (rewritten, v2): samples both system-wide and per-process CPU every 500 ms over 30 s windows (first sample discarded; 3 s cool-downs); reports mean/median/SD for whole-system and workload-attributed power; embeds the full methodology block (power model equations, isolation statement, limitations) in the output JSON; the heavy-computation workload is now genuinely CPU-bound; production/optimization figures are explicitly labelled scenario analyses and computed from attributed energy.
  • caching_benchmark.py: fixed seed (42); each scenario repeated 5 times; JSON reports mean +/- SD for speedup and hit rate plus per-run speedups; header note documents simulated latencies and exact-match semantics.
  • scalability_benchmark.py: fixed seed (42); initialization benchmark repeated 5 times with mean +/- SD; token counting upgraded to tiktoken cl100k_base with chars/4 fallback; JSON records the estimation method and the mock/simulated nature of latencies.
  • token_benchmark.py: JSON now records the tokenizer, the exact reduction formula, and a determinism note; console output uses two decimals.
  • serialization_benchmark.py: JSON now carries a spec-compliance note (JSON-RPC mandated; MessagePack/Pickle prospective; Pickle unsafe for untrusted input).
  • analyze_results.py / visualize_results.py: two-decimal reductions (no more impossible 100.0%); energy table/figure use process-attributed power with SD error bars and honest "estimated" titles; caching chart gains SD error bars; both remain backward-compatible with v1 result files.

Headline numbers: before vs. after re-run (July 5, 2026)

Metric Old (v1, unseeded/contaminated) New (v2, seeded, attributed)
Token reduction (1,000 items) "100.0%" (display artifact) 99.95%
Caching speedup (very high repetition) 18.8x (single run) 12.4 +/- 2.6x (5 seeded runs)
Caching speedup (low repetition) 4.7x 5.0 +/- 0.2x
Tool-definition tokens @ 100 servers 156,075 (chars/4) 142,202 (tiktoken, exact)
Context usage @ 25 servers (GPT-4) 30.4% 27.8%
Idle vs. mixed workload power 26.4 W vs 22.6 W (anomaly) 0.00 W vs 0.43 W attributed (correct ordering)
Serialization (large, JSON vs MsgPack) 663 vs 444 us 707 vs 464 us (stable)
Annual energy saving scenario 12.63 kWh/node 1.76 kWh/node (attributed basis, assumptions stated)

The old caching and energy numbers were not wrong so much as unreproducible and, in the energy case, artifact-laden; the new numbers are smaller but defensible, seeded, and carry uncertainty estimates. The paper reports only the new numbers.

C. Answers to the Review's "Questions for Authors"

Q1. Energy methodology: how were measurements obtained/controlled, and how can a mixed workload consume less than idle? Power was never physically measured; v1 used a CPU-utilisation proxy (P = 0.3TDP + 0.7TDPu_sys) sampled from system-wide utilisation at 500 ms intervals. The "mixed < idle" result was an artifact of background OS activity contaminating the idle window (raw log: 78.6% CPU spike during idle). The v2 methodology attributes power per process: P_attr = 0.7TDP*(u_proc/n_cores). Under attribution, idle = 0.00 W and every active workload exceeds it. Sampling rate, window length, warm-up discard, cool-downs, and the model equations are now in the paper (Section VI-A) and in results/energy_benchmark_results.json. We state explicitly that absolute values are estimates and that relative comparisons are the meaningful quantity; RAPL/wall-meter validation is future work.

Q2. Harness/controller specifications, models, trials, variance? There is no harness: no experiment in the paper places an LLM controller in the loop, by design, precisely because harness configuration dominates variance in end-to-end evaluations (arXiv:2605.23950, now cited). Our measurements are protocol/payload-level: tiktoken cl100k_base token counts (deterministic, zero variance), perf_counter timings (5,000 iterations, SDs logged), and seeded workloads (seed 42, 5 repetitions, mean +/- SD in every affected table). The only model-dependent numbers (MCP-Universe success rates) are quoted from the published third-party benchmark and labelled as such.

Q3. Statistical integrity of "exactly 100.0%" values? Rounding artifact, not fabrication. Raw logs (committed in results/) show 99.95% at 1,000 items; the display format was %.1f. The formula is (T_v - T_o)/T_v * 100; the policy is now two-decimal reporting everywhere; raw JSONs are public in this repository for auditing.

Q4. Are the MessagePack/Pickle results a spec-extension proposal? Yes, and the paper now says so explicitly. They are labelled prospective/non-compliant in the subsection title, body, and table caption. The requested discussion was added: backward compatibility via capability negotiation with JSON-RPC fallback; security review of binary parsers (and Pickle explicitly excluded as unsafe); observability costs of losing human-readable payloads; incremental path restricting binary framing to high-volume resource payloads.

Q5. Which offloading direction is intended? Off the LLM, always. The "to the LLM" sentence was an error, now corrected. The paper defines two forms (data-plane: code execution moves bulk data out of the context window; compute-plane: solver servers move exact mathematics from GPU inference to CPU solvers) and gives the task regimes for each (formal structure -> solvers; open-ended transformation -> code execution).

Q6. Experimental details for CA-MCP (45.5%) and REP (41.8%/47%)? These are the proposing authors' published results, not ours, and the revised paper says so in plain words. Our suite contains no multi-agent workloads; the figures are quoted with attribution, the illustrative chart is captioned as literature-derived, and the paper notes that the original studies do not disclose harness details at the level our measurement card requires. Independent reproduction is listed under future research (audited, harness-disclosed multi-agent benchmarks).

Q7. Interaction with MemAct-style memory policies and SWE-Pruner-style pruning? They are complementary and now integrated in the paper: model-side learned memory editing and goal-conditioned pruning attack the same token economy from the client side, while our server-side techniques (progressive disclosure, hierarchical discovery, caching) attack it from the protocol side. Section VII proposes the concrete integration the reviewer hints at: embedding goal-conditioned pruning inside MCP resource servers as standardised pre-filtered read operations, so servers return pruned payloads instead of raw files. This is also listed as a future research direction.

D. Additional Questions a TSUSC Reviewer Might Ask (with answers)

D1. Why is this in scope for TSUSC and not a survey? The paper's core is an original, reproducible empirical study of energy-relevant protocol behaviour (tokens, transport, caching, scaling, estimated power) plus a taxonomy and derived recommendations. The related-work coverage supports, but does not replace, the original measurements. The title, abstract, and contributions were rewritten accordingly, because TSUSC explicitly rejects surveys.

D2. Why not use RAPL or a wall-power meter? The experiments ran on Windows 11, where RAPL access is not natively exposed, and no hardware meter was available. Rather than present proxy numbers as measurements, the paper (i) publishes the exact power model, (ii) reports process-attributed values with SDs, (iii) restricts claims to relative comparisons, and (iv) names hardware validation as future work. The benchmark scripts run unchanged on Linux, where readers can add RAPL cross-checks.

D3. Why are JSON processing and heavy computation nearly equal in attributed power (~3.8 W)? Both are single-threaded and saturate one logical processor out of twelve, so the utilisation model assigns them nearly identical draw. The paper explains this and draws the correct conclusion: at equal power, what differs is useful work per joule, which is the argument for solver offloading.

D4. Aren't the caching speedups just a function of your simulated latencies? Yes, deliberately: simulated 50/75/100 ms service times isolate protocol/cache behaviour from network noise and bound the achievable speedup, which is why we report 12.4x rather than the literature's "up to 100x". This bounding is stated in the paper.

D5. Token counts use cl100k_base; do results transfer to other tokenizers? The ratios (the quantity we claim) are robust because both verbose and optimised payloads are tokenized identically; absolute counts vary by a small constant factor across modern BPE tokenizers. Byte counts are also reported in the JSON as a tokenizer-independent check.

D6. Is a 1.76 kWh/node/year saving significant? Per node it is modest, and the paper does not inflate it: the estimate covers server-side CPU only and explicitly excludes LLM-side inference energy, where the same token reductions act on a cost that is orders of magnitude larger and quadratic in context length. The server-side figure demonstrates the accounting method; the inference-side savings are the dominant effect.

D7. Can everything be reproduced? Yes: python run_all_benchmarks.py regenerates all JSONs, python visualize_results.py regenerates all figures, and python analyze_results.py regenerates the LaTeX tables. Seeds are fixed; the energy benchmark is the only wall-clock-sensitive component (about 2.5 minutes). On Windows, set PYTHONUTF8=1 to avoid console encoding errors. Requirements: psutil, tiktoken, msgpack, matplotlib, numpy.

D8. Who wrote the paper? The three listed human authors. No AI system is an author, collaborator, or acknowledged contributor.

E. Verification Notes

  • All six review-suggested references were verified to exist on arXiv before citation (titles and author lists checked on July 5, 2026): 2605.23950, 2510.12635, 2605.06716, 2601.16746, 2604.06132, 2605.15338.
  • The full benchmark suite was re-run end-to-end on July 5, 2026 (368 s total, all five benchmarks passing) on the same i5-10400 machine used for the original results; every number in the paper's Section VI was transcribed from the fresh JSONs in results/.
  • Remaining pre-submission tasks (ORCID registration, IEEE LaTeX Analyzer, final page-count check, cover letter) are listed in IEEE_TSUSC_Submission_Guidelines.md at the repository root.

Proposed Method: Graph-Connected Hierarchical Discovery (GHD)

Added July 6, 2026. This is the paper's flagship technical contribution (Section VII of main.tex), created to move the paper beyond taxonomy-plus-microbenchmarks and give it a novel, evaluated mechanism. Script: hierarchical_discovery_benchmark.py. Result log: results/hierarchical_discovery_results.json. Figure: results/figures/hierarchical_discovery.{png,pdf}. Table: results/latex/ghd_table.tex.

What GHD is

A discovery layer that organises MCP tools and Agent-to-Agent (A2A) capability cards into one searchable hierarchy:

  1. Multi-feature embedding. Each entity becomes a vector: TF-IDF -> truncated SVD semantic embedding (L2-normalised) concatenated with standardised operational features (log latency, energy score, log popularity, entity-kind flag) at a small weight so semantics dominate the geometry.
  2. Density-based hierarchical clustering. HDBSCAN (Campello et al., 2013) discovers clusters from the data instead of vendor server/category boundaries, so functionally related tools from different servers group together. Noise points are grafted to the nearest centroid.
  3. Cluster summaries. Each cluster compresses to a ~30-token natural-language summary. Only summaries live permanently in context, giving the agent a persistent map of the whole capability landscape (~1,800 tokens for ~53 clusters).
  4. Lateral graph edges. Each cluster links to its two most similar peers by centroid similarity. Discovery expands the best cluster plus its top graph neighbour, catching cross-domain queries.
  5. Two-stage discovery + incremental sync. Rank summaries, expand the top clusters, inject only the top member definitions. When the agent passes an operational preference, re-rank functional matches by that axis. New tools insert incrementally (nearest-centroid); full re-clustering is the periodic repair.

How it is positioned against prior work (verified citations)

  • RAG-MCP (Gan & Sun, arXiv:2505.03275): flat semantic retrieval over tools; >50% prompt-token reduction. GHD's RET-5/RET-B baselines model this. GHD adds structure (persistent map, operational routing, cheap sync) that flat retrieval lacks.
  • MCP-Zero (Fei et al., arXiv:2506.01056): two-stage semantic routing; ~98% token reduction over ~3,000 tools. GHD differs by clustering on multiple features (semantic + operational) and being data-driven rather than a fixed semantic router.
  • AnyTool (Du et al., ICML 2024, arXiv:2402.04253): hierarchical retriever over a human-curated category tree of 16k+ APIs. GHD's hierarchy is discovered by density-based clustering, not hand-defined, and unifies tools with A2A agents.

Novelty claim (honest): GHD is the first discovery layer to combine (a) density-based, data-driven hierarchy, (b) multi-feature (semantic + operational) indexing enabling constraint-aware selection, (c) a unified tool + A2A-agent space, and (d) cheap incremental synchronisation.

Experimental design

  • Corpus: 1,000 MCP tools + 120 A2A capability cards across 12 domains, 2-3 servers/domain (so functionally equivalent tools with different operational profiles coexist, as in real deployments). Popularity is Zipf-like so queries mirror real usage concentration.
  • 5 seeds (42-46), mean +/- SD reported. No LLM in the loop: "recall" = whether the correct entity is exposed to the model, which isolates the discovery mechanism from harness effects (consistent with the main paper's measurement card).
  • Baselines: FLAT (all definitions in context), RET-5 (top-5 semantic retrieval), RET-B (budget-matched semantic retrieval), GHD-NG (GHD without graph edges, ablation), GHD.
  • Three experiments: (A) semantic discovery tokens/recall; (B) operational-constraint routing (pick cheapest among functionally equivalent tools); (C) synchronisation cost.

Results (mean over 5 seeds)

Policy Tokens/query Semantic recall vs FLAT tokens
Flat (all definitions) 135,462 100.0% baseline
Top-5 retrieval 650 28.5 +/- 9.0% -99.5%
Budget-matched retrieval 4,493 66.7 +/- 4.1% -96.7%
GHD without graph (ablation) 4,525 63.6 +/- 3.6% -96.7%
GHD (proposed) 4,524 64.8 +/- 3.3% -96.7%
  • Experiment A (semantic recall). GHD (64.8%) is statistically indistinguishable from budget-matched flat retrieval (66.7%) at equal token budget, and both crush top-5 retrieval (28.5%). GHD does NOT beat flat retrieval on pure semantics, and the paper says so explicitly. Parity is the honest and expected result.
  • Experiment B (operational-constraint routing). GHD selects the constraint-correct tool (cheapest by latency/energy among functional equivalents) as its top result 46.3% of the time vs 23.6% for semantic retrieval, which ranks by text alone and cannot see operational cost. GHD nearly doubles the correct-selection rate. This is the genuine multi-feature payoff.
  • Experiment C (synchronisation). Incremental insert ~0.6 ms vs full re-cluster ~0.20 s, so incremental maintenance is over 300x cheaper. A new tool routes correctly ~54% of the time before periodic repair.
  • Ablation. Graph edges add a small consistent recall gain (63.6% -> 64.8%).

Honesty notes / how the design was hardened

The first implementation had GHD losing to the budget-matched baseline (20.7% vs 47.3%) because it clustered in the joint semantic+operational space but routed queries in text-only space and expanded a single cluster. Rather than cherry-pick, the design was fixed: (1) semantics-dominant clustering (operational weight 0.05) so functionally equivalent tools co-locate; (2) route to the top-3 clusters plus the best cluster's graph neighbour; (3) two-stage operational selection (identify functional matches within a text-similarity band, then order by the operational axis). After the fix GHD reaches parity on semantics and wins on constraint routing and sync. The full before/after is reproducible by reverting the constants at the top of the script.

Reproduce

pip install -r requirements.txt        # adds scikit-learn for HDBSCAN
set PYTHONUTF8=1                        # Windows only
python hierarchical_discovery_benchmark.py

Runtime ~4 s for 5 seeds. Regenerates the JSON, the 3-panel figure, and the LaTeX table. The whole suite (including GHD) also runs via python run_all_benchmarks.py.