Skip to content

Commit fdc8886

Browse files
authored
Merge pull request #11 from Janix-ai/stage
Stage
2 parents d9b947a + 47a8cc8 commit fdc8886

21 files changed

+5676
-1040
lines changed

mcp_testing/scripts/compliance_report.py

Lines changed: 149 additions & 133 deletions
Large diffs are not rendered by default.
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025 Scott Wilcox
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
"""
6+
HTTP Compliance Report Generator
7+
8+
This script runs the full compliance test suite against an HTTP-based MCP server.
9+
It uses the same comprehensive test cases as the STDIO compliance report but adapts
10+
them to use HTTP transport.
11+
"""
12+
13+
import argparse
14+
import asyncio
15+
import json
16+
import os
17+
import sys
18+
import time
19+
from datetime import datetime
20+
from pathlib import Path
21+
22+
# Add the parent directory to the Python path
23+
parent_dir = Path(__file__).resolve().parent.parent.parent
24+
sys.path.append(str(parent_dir))
25+
26+
from mcp_testing.utils.runner import run_tests
27+
from mcp_testing.utils.reporter import results_to_markdown, extract_server_name, generate_markdown_report
28+
from mcp_testing.tests.base_protocol.test_initialization import TEST_CASES as INIT_TEST_CASES
29+
from mcp_testing.tests.features.test_tools import TEST_CASES as TOOLS_TEST_CASES
30+
from mcp_testing.tests.features.test_async_tools import TEST_CASES as ASYNC_TOOLS_TEST_CASES
31+
from mcp_testing.tests.features.dynamic_tool_tester import TEST_CASES as DYNAMIC_TOOL_TEST_CASES
32+
from mcp_testing.tests.features.dynamic_async_tools import TEST_CASES as DYNAMIC_ASYNC_TEST_CASES
33+
from mcp_testing.tests.specification_coverage import TEST_CASES as SPEC_COVERAGE_TEST_CASES
34+
from mcp_testing.transports.http import HttpTransportAdapter
35+
36+
def log_with_timestamp(message):
37+
"""Log a message with a timestamp prefix."""
38+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
39+
print(f"[{timestamp}] {message}")
40+
41+
async def main():
42+
"""Run the compliance tests using HTTP transport and generate a report."""
43+
parser = argparse.ArgumentParser(description="Generate an MCP HTTP server compliance report")
44+
45+
# Server configuration
46+
parser.add_argument("--server-url", required=True, help="URL of the HTTP server")
47+
parser.add_argument("--protocol-version", choices=["2024-11-05", "2025-03-26"],
48+
default="2025-03-26", help="Protocol version to use")
49+
50+
# Output options
51+
parser.add_argument("--output-dir", default="reports", help="Directory to store the report files")
52+
parser.add_argument("--report-prefix", default="cr", help="Prefix for report filenames (default: 'cr')")
53+
parser.add_argument("--json", action="store_true", help="Generate a JSON report")
54+
55+
# Debug and control options
56+
parser.add_argument("--debug", action="store_true", help="Enable debug output")
57+
parser.add_argument("--skip-async", action="store_true", help="Skip async tests")
58+
parser.add_argument("--skip-tests", help="Comma-separated list of test names to skip")
59+
parser.add_argument("--dynamic-only", action="store_true", help="Only run dynamic tools tests")
60+
parser.add_argument("--test-mode", choices=["all", "core", "tools", "async", "spec"], default="all",
61+
help="Test mode: all, core, tools, async, or spec")
62+
parser.add_argument("--spec-coverage-only", action="store_true",
63+
help="Only run tests for spec coverage")
64+
parser.add_argument("--test-timeout", type=int, default=30,
65+
help="Timeout in seconds for individual tests")
66+
parser.add_argument("--tools-timeout", type=int, default=30,
67+
help="Timeout in seconds for tools tests")
68+
parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
69+
70+
args = parser.parse_args()
71+
72+
# Parse tests to skip
73+
skip_tests = []
74+
if args.skip_tests:
75+
skip_tests = [t.strip() for t in args.skip_tests.split(',')]
76+
77+
if skip_tests:
78+
log_with_timestamp(f"Skipping tests: {', '.join(skip_tests)}")
79+
80+
# Ensure output directory exists
81+
output_dir = os.path.join(parent_dir, args.output_dir)
82+
os.makedirs(output_dir, exist_ok=True)
83+
84+
# Generate timestamp for report filenames
85+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
86+
87+
# Collect test cases based on test mode and flags
88+
tests = []
89+
90+
if args.dynamic_only:
91+
log_with_timestamp("Running in dynamic-only mode - tests will adapt to the server's capabilities")
92+
tests.extend(INIT_TEST_CASES) # Always include initialization tests
93+
tests.extend(DYNAMIC_TOOL_TEST_CASES)
94+
95+
if args.protocol_version == "2025-03-26" and not args.skip_async:
96+
tests.extend(DYNAMIC_ASYNC_TEST_CASES)
97+
98+
elif args.spec_coverage_only:
99+
log_with_timestamp("Running specification coverage tests only")
100+
tests.extend(SPEC_COVERAGE_TEST_CASES)
101+
102+
else:
103+
# Normal mode - collect tests based on test_mode
104+
if args.test_mode in ["all", "core"]:
105+
tests.extend(INIT_TEST_CASES)
106+
107+
if args.test_mode in ["all", "tools"]:
108+
tests.extend(TOOLS_TEST_CASES)
109+
110+
if args.test_mode in ["all", "async"] and args.protocol_version == "2025-03-26" and not args.skip_async:
111+
tests.extend(ASYNC_TOOLS_TEST_CASES)
112+
113+
if args.test_mode in ["all", "spec"]:
114+
tests.extend(SPEC_COVERAGE_TEST_CASES)
115+
116+
# Filter out tests to skip
117+
if skip_tests:
118+
original_count = len(tests)
119+
tests = [(func, name) for func, name in tests if name not in skip_tests]
120+
skipped_count = original_count - len(tests)
121+
if skipped_count > 0:
122+
log_with_timestamp(f"Skipped {skipped_count} tests based on configuration")
123+
124+
log_with_timestamp(f"Running compliance tests for protocol {args.protocol_version}...")
125+
log_with_timestamp(f"Server URL: {args.server_url}")
126+
log_with_timestamp(f"Test mode: {args.test_mode}")
127+
log_with_timestamp(f"Total tests to run: {len(tests)}")
128+
129+
# Create HTTP transport adapter
130+
transport = HttpTransportAdapter(
131+
server_url=args.server_url,
132+
debug=args.debug,
133+
timeout=args.test_timeout
134+
)
135+
136+
# Run the tests
137+
start_time = time.time()
138+
139+
# Set timeouts based on arguments
140+
test_timeout = args.test_timeout
141+
tools_timeout = args.tools_timeout
142+
143+
# Group tests by type and run with appropriate timeouts
144+
tool_tests = [(func, name) for func, name in tests if name.startswith("test_tool_") or name.startswith("test_tools_")]
145+
non_tool_tests = [(func, name) for func, name in tests if not (name.startswith("test_tool_") or name.startswith("test_tools_"))]
146+
147+
results = {
148+
"results": [],
149+
"total": len(tests),
150+
"passed": 0,
151+
"failed": 0,
152+
"skipped": 0,
153+
"timeouts": 0
154+
}
155+
156+
# Run non-tool tests first with standard timeout
157+
if non_tool_tests:
158+
log_with_timestamp(f"Running {len(non_tool_tests)} non-tool tests with {test_timeout}s timeout")
159+
non_tool_results = await run_tests(
160+
non_tool_tests,
161+
protocol=args.protocol_version,
162+
transport="http", # Use HTTP transport
163+
server_command=args.server_url, # Pass server URL as the command
164+
env_vars={}, # No environment variables needed for HTTP
165+
debug=args.debug,
166+
timeout=test_timeout
167+
)
168+
results["results"].extend(non_tool_results["results"])
169+
results["passed"] += non_tool_results["passed"]
170+
results["failed"] += non_tool_results["failed"]
171+
results["skipped"] += non_tool_results.get("skipped", 0)
172+
results["timeouts"] += non_tool_results.get("timeouts", 0)
173+
174+
# Run tool tests with extended timeout
175+
if tool_tests:
176+
log_with_timestamp(f"Running {len(tool_tests)} tool tests with {tools_timeout}s timeout")
177+
tool_results = await run_tests(
178+
tool_tests,
179+
protocol=args.protocol_version,
180+
transport="http", # Use HTTP transport
181+
server_command=args.server_url, # Pass server URL as the command
182+
env_vars={}, # No environment variables needed for HTTP
183+
debug=args.debug,
184+
timeout=tools_timeout
185+
)
186+
results["results"].extend(tool_results["results"])
187+
results["passed"] += tool_results["passed"]
188+
results["failed"] += tool_results["failed"]
189+
results["skipped"] += tool_results.get("skipped", 0)
190+
results["timeouts"] += tool_results.get("timeouts", 0)
191+
192+
# Calculate compliance percentage
193+
total_tests = results["total"] - results["skipped"]
194+
compliance_percentage = (results["passed"] / total_tests) * 100 if total_tests > 0 else 0
195+
196+
# Determine compliance status
197+
if compliance_percentage == 100:
198+
compliance_status = "✅ Fully Compliant"
199+
elif compliance_percentage >= 80:
200+
compliance_status = "⚠️ Mostly Compliant"
201+
else:
202+
compliance_status = "❌ Non-Compliant"
203+
204+
log_with_timestamp("\nCompliance Test Results:")
205+
log_with_timestamp(f"Total tests: {results['total']}")
206+
log_with_timestamp(f"Passed: {results['passed']}")
207+
log_with_timestamp(f"Failed: {results['failed']}")
208+
log_with_timestamp(f"Skipped: {results['skipped']}")
209+
log_with_timestamp(f"Compliance Status: {compliance_status} ({compliance_percentage:.1f}%)")
210+
211+
# Generate report filename
212+
server_name = "HTTP MCP Server" # Generic name for HTTP server
213+
report_basename = f"cr_{server_name}_{args.protocol_version}_{timestamp}"
214+
215+
# Generate markdown report
216+
markdown_lines = [
217+
f"# {server_name} MCP Compliance Report",
218+
"",
219+
"## Server Information",
220+
"",
221+
f"- **Server URL**: `{args.server_url}`",
222+
f"- **Protocol Version**: {args.protocol_version}",
223+
f"- **Test Date**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
224+
"",
225+
"## Summary",
226+
"",
227+
f"- **Total Tests**: {results['total']}",
228+
f"- **Passed**: {results['passed']} ({(results['passed'] / total_tests * 100) if total_tests > 0 else 0:.1f}%)",
229+
f"- **Failed**: {results['failed']} ({(results['failed'] / total_tests * 100) if total_tests > 0 else 0:.1f}%)",
230+
f"- **Skipped**: {results['skipped']}",
231+
"",
232+
f"**Compliance Status**: {compliance_status} ({compliance_percentage:.1f}%)",
233+
"",
234+
"## Detailed Results",
235+
"",
236+
"### Passed Tests",
237+
""
238+
]
239+
240+
# Add passed tests
241+
passed_tests = [r for r in results["results"] if r.get("passed", False)]
242+
if passed_tests:
243+
markdown_lines.append("| Test | Duration | Message |")
244+
markdown_lines.append("|------|----------|---------|")
245+
for test in passed_tests:
246+
test_name = test.get("name", "").replace("test_", "").replace("_", " ").title()
247+
duration = f"{test.get('duration', 0):.2f}s"
248+
message = test.get("message", "")
249+
markdown_lines.append(f"| {test_name} | {duration} | {message} |")
250+
else:
251+
markdown_lines.append("No tests passed.")
252+
253+
markdown_lines.extend([
254+
"",
255+
"### Failed Tests",
256+
""
257+
])
258+
259+
# Add failed tests
260+
failed_tests = [r for r in results["results"] if not r.get("passed", False)]
261+
if failed_tests:
262+
markdown_lines.append("| Test | Duration | Error Message |")
263+
markdown_lines.append("|------|----------|--------------|")
264+
for test in failed_tests:
265+
test_name = test.get("name", "").replace("test_", "").replace("_", " ").title()
266+
duration = f"{test.get('duration', 0):.2f}s"
267+
message = test.get("message", "")
268+
markdown_lines.append(f"| {test_name} | {duration} | {message} |")
269+
else:
270+
markdown_lines.append("All tests passed! 🎉")
271+
272+
# Write markdown report
273+
markdown_content = "\n".join(markdown_lines)
274+
markdown_report_path = os.path.join(output_dir, f"{report_basename}.md")
275+
with open(markdown_report_path, "w") as f:
276+
f.write(markdown_content)
277+
278+
log_with_timestamp(f"Markdown compliance report generated: {markdown_report_path}")
279+
280+
# Generate JSON report if requested
281+
if args.json:
282+
json_report = {
283+
"server": server_name,
284+
"server_url": args.server_url,
285+
"protocol_version": args.protocol_version,
286+
"timestamp": timestamp,
287+
"total_tests": results["total"],
288+
"passed_tests": results["passed"],
289+
"failed_tests": results["failed"],
290+
"skipped_tests": results["skipped"],
291+
"compliance_percentage": compliance_percentage,
292+
"compliance_status": compliance_status,
293+
"results": results["results"]
294+
}
295+
296+
json_report_path = os.path.join(output_dir, f"{report_basename}.json")
297+
with open(json_report_path, "w") as f:
298+
json.dump(json_report, f, indent=2)
299+
300+
log_with_timestamp(f"JSON report saved to: {json_report_path}")
301+
302+
# Return success if fully compliant
303+
return 0 if compliance_percentage == 100 else 1
304+
305+
if __name__ == "__main__":
306+
sys.exit(asyncio.run(main()))

0 commit comments

Comments
 (0)