mirror of
https://github.com/samvallad33/vestige.git
synced 2026-05-08 23:32:37 +02:00
Initial commit: Vestige v1.0.0 - Cognitive memory MCP server
FSRS-6 spaced repetition, spreading activation, synaptic tagging, hippocampal indexing, and 130 years of memory research. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
f9c60eb5a7
169 changed files with 97206 additions and 0 deletions
174
crates/vestige-mcp/src/protocol/messages.rs
Normal file
174
crates/vestige-mcp/src/protocol/messages.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
//! MCP Protocol Messages
|
||||
//!
|
||||
//! Request and response types for MCP methods.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZE
|
||||
// ============================================================================
|
||||
|
||||
/// Initialize request from client
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializeRequest {
|
||||
pub protocol_version: String,
|
||||
pub capabilities: ClientCapabilities,
|
||||
pub client_info: ClientInfo,
|
||||
}
|
||||
|
||||
impl Default for InitializeRequest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
protocol_version: "2024-11-05".to_string(),
|
||||
capabilities: ClientCapabilities::default(),
|
||||
client_info: ClientInfo {
|
||||
name: "unknown".to_string(),
|
||||
version: "0.0.0".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub roots: Option<HashMap<String, Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sampling: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// Initialize response to client
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializeResult {
|
||||
pub protocol_version: String,
|
||||
pub server_info: ServerInfo,
|
||||
pub capabilities: ServerCapabilities,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub instructions: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<HashMap<String, Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<HashMap<String, Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompts: Option<HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TOOLS
|
||||
// ============================================================================
|
||||
|
||||
/// Tool description for tools/list
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolDescription {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub input_schema: Value,
|
||||
}
|
||||
|
||||
/// Result of tools/list
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListToolsResult {
|
||||
pub tools: Vec<ToolDescription>,
|
||||
}
|
||||
|
||||
/// Request for tools/call
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CallToolRequest {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub arguments: Option<Value>,
|
||||
}
|
||||
|
||||
/// Result of tools/call
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CallToolResult {
|
||||
pub content: Vec<ToolResultContent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolResultContent {
|
||||
#[serde(rename = "type")]
|
||||
pub content_type: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RESOURCES
|
||||
// ============================================================================
|
||||
|
||||
/// Resource description for resources/list
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourceDescription {
|
||||
pub uri: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of resources/list
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListResourcesResult {
|
||||
pub resources: Vec<ResourceDescription>,
|
||||
}
|
||||
|
||||
/// Request for resources/read
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadResourceRequest {
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
/// Result of resources/read
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadResourceResult {
|
||||
pub contents: Vec<ResourceContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourceContent {
|
||||
pub uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blob: Option<String>,
|
||||
}
|
||||
7
crates/vestige-mcp/src/protocol/mod.rs
Normal file
7
crates/vestige-mcp/src/protocol/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! MCP Protocol Implementation
|
||||
//!
|
||||
//! JSON-RPC 2.0 over stdio for the Model Context Protocol.
|
||||
|
||||
pub mod messages;
|
||||
pub mod stdio;
|
||||
pub mod types;
|
||||
84
crates/vestige-mcp/src/protocol/stdio.rs
Normal file
84
crates/vestige-mcp/src/protocol/stdio.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
//! stdio Transport for MCP
|
||||
//!
|
||||
//! Handles JSON-RPC communication over stdin/stdout.
|
||||
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use super::types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
|
||||
use crate::server::McpServer;
|
||||
|
||||
/// stdio Transport for MCP server
|
||||
pub struct StdioTransport;
|
||||
|
||||
impl StdioTransport {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Run the MCP server over stdio
|
||||
pub async fn run(self, mut server: McpServer) -> Result<(), io::Error> {
|
||||
let stdin = io::stdin();
|
||||
let stdout = io::stdout();
|
||||
|
||||
let reader = BufReader::new(stdin.lock());
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("Failed to read line: {}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Received: {}", line);
|
||||
|
||||
// Parse JSON-RPC request
|
||||
let request: JsonRpcRequest = match serde_json::from_str(&line) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!("Failed to parse request: {}", e);
|
||||
let error_response = JsonRpcResponse::error(None, JsonRpcError::parse_error());
|
||||
match serde_json::to_string(&error_response) {
|
||||
Ok(response_json) => {
|
||||
writeln!(stdout, "{}", response_json)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize error response: {}", e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the request
|
||||
if let Some(response) = server.handle_request(request).await {
|
||||
match serde_json::to_string(&response) {
|
||||
Ok(response_json) => {
|
||||
debug!("Sending: {}", response_json);
|
||||
writeln!(stdout, "{}", response_json)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to serialize response: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StdioTransport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
201
crates/vestige-mcp/src/protocol/types.rs
Normal file
201
crates/vestige-mcp/src/protocol/types.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//! MCP JSON-RPC Types
|
||||
//!
|
||||
//! Core types for JSON-RPC 2.0 protocol used by MCP.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// MCP Protocol Version
|
||||
pub const MCP_VERSION: &str = "2025-11-25";
|
||||
|
||||
/// JSON-RPC version
|
||||
pub const JSONRPC_VERSION: &str = "2.0";
|
||||
|
||||
// ============================================================================
|
||||
// JSON-RPC REQUEST/RESPONSE
|
||||
// ============================================================================
|
||||
|
||||
/// JSON-RPC Request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JsonRpcRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
|
||||
/// JSON-RPC Response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JsonRpcResponse {
|
||||
pub jsonrpc: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<Value>,
|
||||
#[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: Option<Value>, result: Value) -> Self {
|
||||
Self {
|
||||
jsonrpc: JSONRPC_VERSION.to_string(),
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(id: Option<Value>, error: JsonRpcError) -> Self {
|
||||
Self {
|
||||
jsonrpc: JSONRPC_VERSION.to_string(),
|
||||
id,
|
||||
result: None,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON-RPC ERROR
|
||||
// ============================================================================
|
||||
|
||||
/// JSON-RPC Error Codes (standard + MCP-specific)
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ErrorCode {
|
||||
// Standard JSON-RPC errors
|
||||
ParseError = -32700,
|
||||
InvalidRequest = -32600,
|
||||
MethodNotFound = -32601,
|
||||
InvalidParams = -32602,
|
||||
InternalError = -32603,
|
||||
|
||||
// MCP-specific errors (-32000 to -32099)
|
||||
ConnectionClosed = -32000,
|
||||
RequestTimeout = -32001,
|
||||
ResourceNotFound = -32002,
|
||||
ServerNotInitialized = -32003,
|
||||
}
|
||||
|
||||
impl From<ErrorCode> for i32 {
|
||||
fn from(code: ErrorCode) -> Self {
|
||||
code as i32
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON-RPC Error
|
||||
#[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>,
|
||||
}
|
||||
|
||||
impl JsonRpcError {
|
||||
fn new(code: ErrorCode, message: &str) -> Self {
|
||||
Self {
|
||||
code: code.into(),
|
||||
message: message.to_string(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_error() -> Self {
|
||||
Self::new(ErrorCode::ParseError, "Parse error")
|
||||
}
|
||||
|
||||
pub fn method_not_found() -> Self {
|
||||
Self::new(ErrorCode::MethodNotFound, "Method not found")
|
||||
}
|
||||
|
||||
pub fn method_not_found_with_message(message: &str) -> Self {
|
||||
Self::new(ErrorCode::MethodNotFound, message)
|
||||
}
|
||||
|
||||
pub fn invalid_params(message: &str) -> Self {
|
||||
Self::new(ErrorCode::InvalidParams, message)
|
||||
}
|
||||
|
||||
pub fn internal_error(message: &str) -> Self {
|
||||
Self::new(ErrorCode::InternalError, message)
|
||||
}
|
||||
|
||||
pub fn server_not_initialized() -> Self {
|
||||
Self::new(ErrorCode::ServerNotInitialized, "Server not initialized")
|
||||
}
|
||||
|
||||
pub fn resource_not_found(uri: &str) -> Self {
|
||||
Self::new(ErrorCode::ResourceNotFound, &format!("Resource not found: {}", uri))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JsonRpcError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "[{}] {}", self.code, self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for JsonRpcError {}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_request_serialization() {
|
||||
let request = JsonRpcRequest {
|
||||
jsonrpc: JSONRPC_VERSION.to_string(),
|
||||
id: Some(Value::Number(1.into())),
|
||||
method: "test".to_string(),
|
||||
params: Some(serde_json::json!({"key": "value"})),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let parsed: JsonRpcRequest = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(parsed.method, "test");
|
||||
assert!(parsed.id.is_some()); // Has id, not a notification
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notification() {
|
||||
let notification = JsonRpcRequest {
|
||||
jsonrpc: JSONRPC_VERSION.to_string(),
|
||||
id: None,
|
||||
method: "notify".to_string(),
|
||||
params: None,
|
||||
};
|
||||
|
||||
assert!(notification.id.is_none()); // No id = notification
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_success() {
|
||||
let response = JsonRpcResponse::success(
|
||||
Some(Value::Number(1.into())),
|
||||
serde_json::json!({"result": "ok"}),
|
||||
);
|
||||
|
||||
assert!(response.result.is_some());
|
||||
assert!(response.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_error() {
|
||||
let response = JsonRpcResponse::error(
|
||||
Some(Value::Number(1.into())),
|
||||
JsonRpcError::method_not_found(),
|
||||
);
|
||||
|
||||
assert!(response.result.is_none());
|
||||
assert!(response.error.is_some());
|
||||
assert_eq!(response.error.unwrap().code, -32601);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue