use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct DocumentedTool {
name: String,
description: String,
required_params: Vec<String>,
optional_params: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct McpResponse {
jsonrpc: String,
id: Option<Value>,
result: Option<Value>,
error: Option<Value>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct ToolDefinition {
name: String,
description: Option<String>,
#[serde(rename = "inputSchema")]
input_schema: Option<Value>,
}
fn parse_documented_mcp_tools() -> Vec<DocumentedTool> {
let doc_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/mcp-methods.md");
let content = match fs::read_to_string(&doc_path) {
Ok(content) => content,
Err(_) => {
eprintln!("Skipping test: mcp-methods.md not found at {:?}", doc_path);
return vec![];
}
};
let mut tools = Vec::new();
let tool_patterns = vec![
(
"generate_template",
"Generate templates with parameter substitution",
),
("get_server_info", "Get information about the server"),
("list_templates", "List available templates"),
("scaffold_project", "Scaffold a complete project"),
("search_templates", "Search for templates"),
("validate_template", "Validate template parameters"),
("analyze_complexity", "Analyze code complexity"),
("analyze_code_churn", "Analyze code change patterns"),
("analyze_dag", "Generate dependency graphs"),
("generate_context", "Generate project context"),
("analyze_dead_code", "Analyze dead code"),
("analyze_deep_context", "Analyze deep context"),
("analyze_satd", "Analyze self-admitted technical debt"),
("analyze_tdg", "Calculate technical debt gradient"),
("analyze_lint_hotspot", "Analyze lint violation hotspots"),
(
"analyze_duplicates_vectorized",
"Analyze code duplicates using SIMD",
),
(
"analyze_graph_metrics_vectorized",
"Analyze graph metrics using vectorization",
),
(
"analyze_name_similarity_vectorized",
"Analyze name similarity using SIMD",
),
(
"analyze_symbol_table_vectorized",
"Analyze symbol tables with vectorization",
),
(
"analyze_incremental_coverage_vectorized",
"Analyze incremental coverage with SIMD",
),
(
"analyze_big_o_vectorized",
"Analyze Big O complexity using vectorization",
),
(
"generate_enhanced_report",
"Generate enhanced analysis report",
),
];
let param_regex = Regex::new(r#""([^"]+)":\s*[^,}]+"#).unwrap();
for (name, description) in tool_patterns {
if content.contains(name) {
let mut required_params = Vec::new();
let mut optional_params = Vec::new();
let json_regex = Regex::new(&format!(
r#""name":\s*"{name}".*?"arguments":\s*\{{([^}}]+)\}}"#
))
.unwrap();
if let Some(cap) = json_regex.captures(&content) {
let args_content = &cap[1];
for param_cap in param_regex.captures_iter(args_content) {
let param_name = param_cap[1].to_string();
if param_name == "resource_uri" || param_name == "project_path" {
required_params.push(param_name);
} else {
optional_params.push(param_name);
}
}
}
tools.push(DocumentedTool {
name: name.to_string(),
description: description.to_string(),
required_params,
optional_params,
});
}
}
tools
}
fn get_binary_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root = Path::new(manifest_dir).parent().unwrap();
let release_binary = workspace_root.join("target/release/pmat");
let debug_binary = workspace_root.join("target/debug/pmat");
if release_binary.exists() {
release_binary.to_string_lossy().to_string()
} else if debug_binary.exists() {
debug_binary.to_string_lossy().to_string()
} else {
"pmat".to_string()
}
}
fn send_mcp_request(request: Value) -> Result<McpResponse, String> {
use std::io::{BufRead, BufReader};
use std::time::{Duration, Instant};
let binary_path = get_binary_path();
let mut child = Command::new(&binary_path)
.env("MCP_VERSION", "1.0")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start MCP server: {e}"))?;
let mut stdin = child.stdin.take().ok_or("Failed to get stdin")?;
let stdout = child.stdout.take().ok_or("Failed to get stdout")?;
let request_str = serde_json::to_string(&request).map_err(|e| e.to_string())?;
stdin
.write_all(request_str.as_bytes())
.map_err(|e| e.to_string())?;
stdin.write_all(b"\n").map_err(|e| e.to_string())?;
stdin.flush().map_err(|e| e.to_string())?;
drop(stdin);
let reader = BufReader::new(stdout);
let start = Instant::now();
let timeout = Duration::from_secs(10);
for line in reader.lines() {
if start.elapsed() > timeout {
let _ = child.kill();
let _ = child.wait();
return Err("Timeout waiting for MCP response".to_string());
}
let line = line.map_err(|e| e.to_string())?;
if line.trim().is_empty() {
continue;
}
if let Ok(response) = serde_json::from_str::<McpResponse>(&line) {
let _ = child.kill();
let _ = child.wait();
return Ok(response);
}
}
let _ = child.kill();
let _ = child.wait();
Err("No valid JSON response found in output".to_string())
}
#[test]
#[ignore = "Slow test - requires binary and MCP server interaction"]
fn test_mcp_tools_match_documentation() {
if std::env::var("CI").is_ok() || std::env::var("SKIP_SLOW_TESTS").unwrap_or_default() == "true"
{
eprintln!("Skipping MCP server test in CI environment");
return;
}
let init_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"capabilities": {}
}
});
let init_response =
send_mcp_request(init_request).expect("Failed to initialize MCP connection");
assert!(
init_response.error.is_none(),
"MCP initialization failed: {:?}",
init_response.error
);
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response = send_mcp_request(tools_request).expect("Failed to get tools list");
assert!(
tools_response.error.is_none(),
"Failed to list tools: {:?}",
tools_response.error
);
let tools_result = tools_response.result.expect("No result in tools response");
let tools_array = tools_result["tools"]
.as_array()
.expect("Tools result should contain a tools array");
let actual_tools: Vec<ToolDefinition> = tools_array
.iter()
.filter_map(|t| serde_json::from_value(t.clone()).ok())
.collect();
let actual_tool_names: Vec<String> = actual_tools.iter().map(|t| t.name.clone()).collect();
let documented_tools = parse_documented_mcp_tools();
for doc_tool in &documented_tools {
assert!(
actual_tool_names.contains(&doc_tool.name),
"Documented MCP tool '{}' not found in actual tools. Available tools: {:?}",
doc_tool.name,
actual_tool_names
);
}
}
#[test]
#[ignore = "Slow test - requires binary and MCP server interaction"]
fn test_mcp_tool_schemas_match_documentation() {
if std::env::var("CI").is_ok() || std::env::var("SKIP_SLOW_TESTS").unwrap_or_default() == "true"
{
eprintln!("Skipping MCP server test in CI environment");
return;
}
let init_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"capabilities": {}
}
});
send_mcp_request(init_request).expect("Failed to initialize");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response = send_mcp_request(tools_request).expect("Failed to get tools list");
let tools_result = tools_response.result.expect("No result in tools response");
let tools_array = tools_result["tools"]
.as_array()
.expect("Tools result should contain a tools array");
let documented_tools = parse_documented_mcp_tools();
for doc_tool in &documented_tools {
let actual_tool = tools_array
.iter()
.find(|t| t["name"].as_str() == Some(&doc_tool.name));
if let Some(tool) = actual_tool {
if let Some(schema) = tool.get("inputSchema") {
if let Some(properties) = schema.get("properties").and_then(|p| p.as_object()) {
for req_param in &doc_tool.required_params {
assert!(
properties.contains_key(req_param),
"Documented required parameter '{}' not found in schema for tool '{}'",
req_param,
doc_tool.name
);
}
}
if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
let actual_required: Vec<String> = required
.iter()
.filter_map(|v| v.as_str().map(std::string::ToString::to_string))
.collect();
for doc_req in &doc_tool.required_params {
assert!(
actual_required.contains(doc_req),
"Documented parameter '{}' should be required for tool '{}' but isn't",
doc_req,
doc_tool.name
);
}
}
}
}
}
}
#[test]
#[ignore = "Documentation structure test - may fail during development"]
fn test_mcp_methods_match_documentation() {
let doc_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/mcp-methods.md");
let content = match fs::read_to_string(&doc_path) {
Ok(content) => content,
Err(_) => {
eprintln!("Skipping test: mcp-methods.md not found at {:?}", doc_path);
return;
}
};
let methods_section = content
.split("### Available MCP Methods")
.nth(1)
.and_then(|s| s.split("###").next())
.expect("Could not find Available MCP Methods section");
let mut documented_methods = Vec::new();
let method_regex = Regex::new(r"`([a-z/]+)`\s*-").unwrap();
for cap in method_regex.captures_iter(methods_section) {
documented_methods.push(cap[1].to_string());
}
let expected_methods = vec![
"initialize",
"tools/list",
"tools/call",
"resources/list",
"resources/read",
"prompts/list",
];
for method in &expected_methods {
assert!(
documented_methods.contains(&(*method).to_string()),
"Expected MCP method '{method}' not documented"
);
}
}
#[test]
#[ignore = "Documentation structure test - may fail during development"]
fn test_mcp_error_codes_are_complete() {
let doc_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("docs/mcp-methods.md");
let content = match fs::read_to_string(&doc_path) {
Ok(content) => content,
Err(_) => {
eprintln!("Skipping test: mcp-methods.md not found at {:?}", doc_path);
return;
}
};
let error_section = content
.split("### Error Codes")
.nth(1)
.and_then(|s| s.split("###").next())
.expect("Could not find Error Codes section");
let mut documented_errors = Vec::new();
let error_regex = Regex::new(r"\|\s*(-?\d+)\s*\|").unwrap();
for cap in error_regex.captures_iter(error_section) {
if let Ok(code) = cap[1].parse::<i32>() {
documented_errors.push(code);
}
}
let standard_errors = vec![-32700, -32600, -32601, -32602];
for error_code in &standard_errors {
assert!(
documented_errors.contains(error_code),
"Standard JSON-RPC error code {error_code} not documented"
);
}
}
#[test]
#[ignore = "Slow test - requires binary and MCP server interaction"]
fn test_no_undocumented_mcp_tools() {
if std::env::var("CI").is_ok() || std::env::var("SKIP_SLOW_TESTS").unwrap_or_default() == "true"
{
eprintln!("Skipping MCP server test in CI environment");
return;
}
let init_request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"capabilities": {}
}
});
send_mcp_request(init_request).expect("Failed to initialize");
let tools_request = json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
});
let tools_response = send_mcp_request(tools_request).expect("Failed to get tools list");
let tools_result = tools_response.result.expect("No result");
let tools_array = tools_result["tools"].as_array().expect("No tools array");
let documented_tools = parse_documented_mcp_tools();
if documented_tools.is_empty() {
eprintln!("No documented tools found, skipping test");
return;
}
let documented_names: Vec<String> = documented_tools.iter().map(|t| t.name.clone()).collect();
for tool in tools_array {
if let Some(name) = tool["name"].as_str() {
if name.starts_with('_') {
continue;
}
assert!(
documented_names.iter().any(|doc_name| doc_name == name),
"MCP tool '{name}' exists but is not documented in mcp-methods.md"
);
}
}
}