Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/engine/language_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,30 @@

use crate::engine::diff_parser;

/// Build language-specific context string from detected languages in the diff.
/// Build language-specific context from already-parsed diff chunks.
///
/// Detects which languages are present and injects relevant rules/patterns
/// into the LLM prompt.
/// Uses pre-parsed chunks to avoid redundant parsing.
pub fn build_language_context_from_chunks(chunks: &[diff_parser::FileChunk]) -> String {
let languages = detect_languages(chunks);

if languages.is_empty() {
return String::new();
}

let mut parts = Vec::new();
parts.push("LANGUAGE-SPECIFIC REVIEW GUIDANCE:".to_string());

for lang in &languages {
if let Some(rules) = get_language_rules(lang) {
parts.push(rules.to_string());
}
}

parts.join("\n\n")
}

/// Build language-specific context string from raw diff text.
#[allow(dead_code)]
pub fn build_language_context(diff_text: &str) -> String {
let chunks = diff_parser::parse_diff(diff_text);
let languages = detect_languages(&chunks);
Expand Down
5 changes: 3 additions & 2 deletions src/engine/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,9 @@ async fn review_diff_inner(
combined_context
};

// Inject language-specific context
let lang_context = crate::engine::language_analyzer::build_language_context(diff);
// Inject language-specific context (reuses parsed diff_chunks)
let lang_context =
crate::engine::language_analyzer::build_language_context_from_chunks(&diff_chunks);
let final_context = if !lang_context.is_empty() {
match final_context {
Some(ctx) => Some(format!("{lang_context}\n\n{ctx}")),
Expand Down
12 changes: 6 additions & 6 deletions src/engine/security_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ use crate::engine::rules::types::RuleFinding;

// ─── Security patterns ───

struct SecurityPattern {
id: &'static str,
name: &'static str,
regex: &'static str,
severity: Severity,
pub struct SecurityPattern {
pub id: &'static str,
pub name: &'static str,
pub regex: &'static str,
pub severity: Severity,
}

static PATTERNS: &[SecurityPattern] = &[
pub static PATTERNS: &[SecurityPattern] = &[
// ── Weak crypto ──
SecurityPattern {
id: "crypto/md5-password",
Expand Down
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod error;
mod formatters;
mod git;
mod hook;
mod mcp;
mod progress;

use commands::{
Expand Down Expand Up @@ -245,6 +246,9 @@ enum Command {
/// Shell name: bash, zsh, fish, or powershell
shell: String,
},

/// Start MCP server (Model Context Protocol for AI agents)
Mcp,
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -501,6 +505,10 @@ async fn main() -> Result<()> {
completion::execute_completion(&shell)?;
0
}
Command::Mcp => {
mcp::server::run_server()?;
0
}
};

std::process::exit(exit_code);
Expand Down
28 changes: 28 additions & 0 deletions src/mcp/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! MCP (Model Context Protocol) server — exposes cora rules and config to AI coding agents.
//!
//! Allows Claude Code, Cursor, Copilot, Windsurf, etc. to query cora's quality
//! rules, project config, and review history in real-time.
//!
//! Transport: stdio (stdin/stdout JSON-RPC)

pub mod protocol;
pub mod server;
pub mod tools;

use serde::{Deserialize, Serialize};

/// MCP server configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct McpConfig {
/// Enable MCP server.
#[serde(default)]
pub enabled: bool,
}

#[allow(clippy::derivable_impls)]
impl Default for McpConfig {
fn default() -> Self {
Self { enabled: false }
}
}
154 changes: 154 additions & 0 deletions src/mcp/protocol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! MCP protocol types — JSON-RPC 2.0 messages for Model Context Protocol.

use serde::{Deserialize, Serialize};

/// JSON-RPC 2.0 request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: Option<RequestId>,
pub method: String,
#[serde(default)]
pub params: serde_json::Value,
}

/// JSON-RPC 2.0 response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RequestId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}

/// JSON-RPC error object.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}

/// Request ID — can be a number or string.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestId {
Number(i64),
String(String),
}

// ─── MCP-specific types ───

/// MCP tool definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Value,
}

/// MCP tool call result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub content: Vec<ContentBlock>,
#[serde(default)]
pub is_error: bool,
}

/// Content block in a tool result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentBlock {
#[serde(rename = "type")]
pub content_type: String,
pub text: String,
}

impl ToolResult {
/// Create a text result.
pub fn text(text: impl Into<String>) -> Self {
Self {
content: vec![ContentBlock {
content_type: "text".to_string(),
text: text.into(),
}],
is_error: false,
}
}

/// Create an error result.
pub fn error(text: impl Into<String>) -> Self {
Self {
content: vec![ContentBlock {
content_type: "text".to_string(),
text: text.into(),
}],
is_error: true,
}
}
}

/// Server info for initialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}

/// Server capabilities.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerCapabilities {
pub tools: serde_json::Value,
}

/// Initialize result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ServerCapabilities,
#[serde(rename = "serverInfo")]
pub server_info: ServerInfo,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_request() {
let req: JsonRpcRequest =
serde_json::from_str(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#)
.unwrap();
assert_eq!(req.method, "tools/list");
}

#[test]
fn serialize_response() {
let resp = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: Some(RequestId::Number(1)),
result: Some(serde_json::json!({"ok": true})),
error: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"result\""));
}

#[test]
fn tool_result_text() {
let result = ToolResult::text("hello");
assert!(!result.is_error);
assert_eq!(result.content[0].text, "hello");
}

#[test]
fn tool_result_error() {
let result = ToolResult::error("fail");
assert!(result.is_error);
}
}
Loading
Loading