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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ members = [
"vajra-domain-encoding",
"vajra-domain-github",
"vajra-report",
"vajra-mcp",
]

[workspace.package]
Expand Down
14 changes: 14 additions & 0 deletions vajra-mcp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "vajra-mcp"
version = "0.1.0"
edition = "2021"
description = "MCP (Model Context Protocol) server for vajra"
license = "MIT OR Apache-2.0"

[[bin]]
name = "vajra-mcp"
path = "src/main.rs"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
10 changes: 10 additions & 0 deletions vajra-mcp/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mod protocol;
mod router;
mod tools;
mod transport;

fn main() {
let registry = tools::register_all();
let router = router::Router::new(registry);
transport::run_stdio(router);
}
220 changes: 220 additions & 0 deletions vajra-mcp/src/protocol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
//! JSON-RPC 2.0 + MCP message types.
//!
//! Minimal implementation of the JSON-RPC 2.0 spec as used by MCP (2025-03-26).

use serde::{Deserialize, Serialize};
use serde_json::Value;

// ─────────────────────────────────────────────────
// Request ID — number or string per JSON-RPC 2.0
// ─────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Id {
Number(i64),
Text(String),
}

// ─────────────────────────────────────────────────
// Incoming message (request or notification)
// ─────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct JsonRpcMessage {
#[allow(dead_code)]
pub jsonrpc: String,
#[serde(default)]
pub id: Option<Id>,
pub method: Option<String>,
#[serde(default)]
pub params: Option<Value>,
}

impl JsonRpcMessage {
pub fn is_notification(&self) -> bool {
self.id.is_none() && self.method.is_some()
}
}

// ─────────────────────────────────────────────────
// Outgoing response
// ─────────────────────────────────────────────────

#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: &'static str,
pub id: Id,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}

impl JsonRpcResponse {
pub fn success(id: Id, result: Value) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(result),
error: None,
}
}

pub fn error(id: Id, error: JsonRpcError) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(error),
}
}
}

// ─────────────────────────────────────────────────
// Error object
// ─────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}

pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;

impl JsonRpcError {
pub fn parse_error(detail: &str) -> Self {
Self {
code: PARSE_ERROR,
message: format!("Parse error: {detail}"),
data: None,
}
}

#[allow(dead_code)]
pub fn invalid_request(detail: &str) -> Self {
Self {
code: INVALID_REQUEST,
message: format!("Invalid request: {detail}"),
data: None,
}
}

pub fn method_not_found(method: &str) -> Self {
Self {
code: METHOD_NOT_FOUND,
message: format!("Method not found: {method}"),
data: None,
}
}

pub fn invalid_params(detail: &str) -> Self {
Self {
code: INVALID_PARAMS,
message: format!("Invalid params: {detail}"),
data: None,
}
}

pub fn internal(detail: &str) -> Self {
Self {
code: INTERNAL_ERROR,
message: format!("Internal error: {detail}"),
data: None,
}
}
}

// ─────────────────────────────────────────────────
// MCP types
// ─────────────────────────────────────────────────

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: &'static str,
pub capabilities: ServerCapabilities,
pub server_info: ServerInfo,
pub instructions: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ServerCapabilities {
pub tools: Option<ToolsCapability>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsCapability {
pub list_changed: bool,
}

#[derive(Debug, Serialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolListResult {
pub tools: Vec<ToolDescriptor>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolDescriptor {
pub name: String,
pub description: String,
pub input_schema: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Value>,
}

#[derive(Debug, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(default)]
pub arguments: Option<Value>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResult {
pub content: Vec<Content>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub is_error: bool,
}

impl CallToolResult {
pub fn text(text: String) -> Self {
Self {
content: vec![Content::Text { text }],
is_error: false,
}
}

pub fn error(message: String) -> Self {
Self {
content: vec![Content::Text { text: message }],
is_error: true,
}
}
}

#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Content {
#[serde(rename = "text")]
Text { text: String },
}

#[derive(Debug, Serialize)]
pub struct PingResult {}
125 changes: 125 additions & 0 deletions vajra-mcp/src/router.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! JSON-RPC method router.
//!
//! Dispatches incoming JSON-RPC messages to the appropriate handler:
//! initialize, tools/list, tools/call, ping, notifications.

use serde_json::Value;

use crate::protocol::*;
use crate::tools::ToolRegistry;

pub struct Router {
tools: ToolRegistry,
}

impl Router {
pub fn new(tools: ToolRegistry) -> Self {
Self { tools }
}

pub fn handle(&self, msg: JsonRpcMessage) -> Option<JsonRpcResponse> {
// Notifications (no id) don't get responses
if msg.is_notification() {
return None;
}

// Requests require an id
let id = msg.id?;

let method = match msg.method.as_deref() {
Some(m) => m,
None => {
return Some(JsonRpcResponse::error(
id,
JsonRpcError::invalid_request("missing method"),
));
}
};

let params = msg.params.unwrap_or(Value::Object(serde_json::Map::new()));

let result = match method {
"initialize" => self.handle_initialize(),
"ping" => self.handle_ping(),
"tools/list" => self.handle_tools_list(),
"tools/call" => self.handle_tools_call(params),
other => {
return Some(JsonRpcResponse::error(
id,
JsonRpcError::method_not_found(other),
));
}
};

match result {
Ok(value) => Some(JsonRpcResponse::success(id, value)),
Err(err) => Some(JsonRpcResponse::error(id, err)),
}
}

fn handle_initialize(&self) -> Result<Value, JsonRpcError> {
let result = InitializeResult {
protocol_version: "2025-03-26",
capabilities: ServerCapabilities {
tools: Some(ToolsCapability {
list_changed: false,
}),
},
server_info: ServerInfo {
name: "vajra".into(),
version: env!("CARGO_PKG_VERSION").into(),
},
instructions: Some(
"Vajra is a deterministic structural-analysis engine for data files \
(JSON, YAML, CSV, NDJSON, Markdown, PDF, source code, git repos). \
It provides structural inspection, statistical summaries, anomaly \
detection, fingerprinting, drift detection, governance metrics, \
health scoring, and full GitHub repository auditing. \
Start with `vajra_inspect` to understand a file's structure, \
then use `vajra_stats`, `vajra_anomalies`, or `vajra_essence` \
for deeper analysis. Use `vajra_audit` for one-command GitHub \
repository health reports."
.into(),
),
};

serde_json::to_value(result).map_err(|e| JsonRpcError::internal(&e.to_string()))
}

fn handle_ping(&self) -> Result<Value, JsonRpcError> {
serde_json::to_value(PingResult {}).map_err(|e| JsonRpcError::internal(&e.to_string()))
}

fn handle_tools_list(&self) -> Result<Value, JsonRpcError> {
let result = ToolListResult {
tools: self.tools.descriptors(),
};
serde_json::to_value(result).map_err(|e| JsonRpcError::internal(&e.to_string()))
}

fn handle_tools_call(&self, params: Value) -> Result<Value, JsonRpcError> {
let call: CallToolParams = serde_json::from_value(params)
.map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?;

let tool = self
.tools
.get(&call.name)
.ok_or_else(|| JsonRpcError::invalid_params(&format!("unknown tool: {}", call.name)))?;

let tool_params = call
.arguments
.unwrap_or(Value::Object(serde_json::Map::new()));

let result = tool.call(tool_params);

match result {
Ok(call_result) => serde_json::to_value(call_result)
.map_err(|e| JsonRpcError::internal(&e.to_string())),
Err(tool_err) => {
let call_result = CallToolResult::error(tool_err.to_string());
serde_json::to_value(call_result)
.map_err(|e| JsonRpcError::internal(&e.to_string()))
}
}
}
}
Loading
Loading