Files
owlen/crates/owlen-core/src/mcp/protocol.rs
vikingowl 05e90d3e2b feat(mcp): add LLM server crate and remote client integration
- Introduce `owlen-mcp-llm-server` crate with RPC handling, `generate_text` tool, model listing, and streaming notifications.
- Add `RpcNotification` struct and `MODELS_LIST` method to the MCP protocol.
- Update `owlen-core` to depend on `tokio-stream`.
- Adjust Ollama provider to omit empty `tools` field for compatibility.
- Enhance `RemoteMcpClient` to locate the renamed server binary, handle resource tools locally, and implement the `Provider` trait (model listing, chat, streaming, health check).
- Add new crate to workspace `Cargo.toml`.
2025-10-09 13:46:33 +02:00

390 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// MCP Protocol Definitions
///
/// This module defines the JSON-RPC protocol contracts for the Model Context Protocol (MCP).
/// It includes request/response schemas, error codes, and versioning semantics.
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// MCP Protocol version - uses semantic versioning
pub const PROTOCOL_VERSION: &str = "1.0.0";
/// JSON-RPC version constant
pub const JSONRPC_VERSION: &str = "2.0";
// ============================================================================
// Error Codes and Handling
// ============================================================================
/// Standard JSON-RPC error codes following the spec
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorCode(pub i64);
impl ErrorCode {
// Standard JSON-RPC 2.0 errors
pub const PARSE_ERROR: Self = Self(-32700);
pub const INVALID_REQUEST: Self = Self(-32600);
pub const METHOD_NOT_FOUND: Self = Self(-32601);
pub const INVALID_PARAMS: Self = Self(-32602);
pub const INTERNAL_ERROR: Self = Self(-32603);
// MCP-specific errors (range -32000 to -32099)
pub const TOOL_NOT_FOUND: Self = Self(-32000);
pub const TOOL_EXECUTION_FAILED: Self = Self(-32001);
pub const PERMISSION_DENIED: Self = Self(-32002);
pub const RESOURCE_NOT_FOUND: Self = Self(-32003);
pub const TIMEOUT: Self = Self(-32004);
pub const VALIDATION_ERROR: Self = Self(-32005);
pub const PATH_TRAVERSAL: Self = Self(-32006);
pub const RATE_LIMIT_EXCEEDED: Self = Self(-32007);
}
/// Structured error response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl RpcError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code: code.0,
message: message.into(),
data: None,
}
}
pub fn with_data(mut self, data: Value) -> Self {
self.data = Some(data);
self
}
pub fn parse_error(message: impl Into<String>) -> Self {
Self::new(ErrorCode::PARSE_ERROR, message)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(ErrorCode::INVALID_REQUEST, message)
}
pub fn method_not_found(method: &str) -> Self {
Self::new(
ErrorCode::METHOD_NOT_FOUND,
format!("Method not found: {}", method),
)
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(ErrorCode::INVALID_PARAMS, message)
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(ErrorCode::INTERNAL_ERROR, message)
}
pub fn tool_not_found(tool_name: &str) -> Self {
Self::new(
ErrorCode::TOOL_NOT_FOUND,
format!("Tool not found: {}", tool_name),
)
}
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(ErrorCode::PERMISSION_DENIED, message)
}
pub fn path_traversal() -> Self {
Self::new(ErrorCode::PATH_TRAVERSAL, "Path traversal attempt detected")
}
}
// ============================================================================
// Request/Response Structures
// ============================================================================
/// JSON-RPC request structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcRequest {
pub jsonrpc: String,
pub id: RequestId,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl RpcRequest {
pub fn new(id: RequestId, method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
method: method.into(),
params,
}
}
}
/// JSON-RPC response structure (success)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcResponse {
pub jsonrpc: String,
pub id: RequestId,
pub result: Value,
}
impl RpcResponse {
pub fn new(id: RequestId, result: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
result,
}
}
}
/// JSON-RPC error response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcErrorResponse {
pub jsonrpc: String,
pub id: RequestId,
pub error: RpcError,
}
impl RpcErrorResponse {
pub fn new(id: RequestId, error: RpcError) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
error,
}
}
}
/// JSONRPC notification (no id). Used for streaming partial results.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl RpcNotification {
pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params,
}
}
}
/// Request ID can be string, number, or null
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(untagged)]
pub enum RequestId {
Number(u64),
String(String),
}
impl From<u64> for RequestId {
fn from(n: u64) -> Self {
Self::Number(n)
}
}
impl From<String> for RequestId {
fn from(s: String) -> Self {
Self::String(s)
}
}
// ============================================================================
// MCP Method Names
// ============================================================================
/// Standard MCP methods
pub mod methods {
pub const INITIALIZE: &str = "initialize";
pub const TOOLS_LIST: &str = "tools/list";
pub const TOOLS_CALL: &str = "tools/call";
pub const RESOURCES_LIST: &str = "resources/list";
pub const RESOURCES_GET: &str = "resources/get";
pub const RESOURCES_WRITE: &str = "resources/write";
pub const RESOURCES_DELETE: &str = "resources/delete";
pub const MODELS_LIST: &str = "models/list";
}
// ============================================================================
// Initialization Protocol
// ============================================================================
/// Initialize request parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeParams {
pub protocol_version: String,
pub client_info: ClientInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<ClientCapabilities>,
}
impl Default for InitializeParams {
fn default() -> Self {
Self {
protocol_version: PROTOCOL_VERSION.to_string(),
client_info: ClientInfo {
name: "owlen".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
capabilities: None,
}
}
}
/// Client information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
/// Client capabilities
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_streaming: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_cancellation: Option<bool>,
}
/// Initialize response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeResult {
pub protocol_version: String,
pub server_info: ServerInfo,
pub capabilities: ServerCapabilities,
}
/// Server information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
/// Server capabilities
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_tools: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_resources: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_streaming: Option<bool>,
}
// ============================================================================
// Tool Call Protocol
// ============================================================================
/// Parameters for tools/list
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolsListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
}
/// Parameters for tools/call
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsCallParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
}
/// Result of tools/call
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsCallResult {
pub success: bool,
pub output: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Value>,
}
// ============================================================================
// Resource Protocol
// ============================================================================
/// Parameters for resources/list
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourcesListParams {
pub path: String,
}
/// Parameters for resources/get
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourcesGetParams {
pub path: String,
}
/// Parameters for resources/write
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourcesWriteParams {
pub path: String,
pub content: String,
}
/// Parameters for resources/delete
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourcesDeleteParams {
pub path: String,
}
// ============================================================================
// Versioning and Compatibility
// ============================================================================
/// Check if a protocol version is compatible
pub fn is_compatible(client_version: &str, server_version: &str) -> bool {
// For now, simple exact match on major version
let client_major = client_version.split('.').next().unwrap_or("0");
let server_major = server_version.split('.').next().unwrap_or("0");
client_major == server_major
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes() {
let err = RpcError::tool_not_found("test_tool");
assert_eq!(err.code, ErrorCode::TOOL_NOT_FOUND.0);
assert!(err.message.contains("test_tool"));
}
#[test]
fn test_version_compatibility() {
assert!(is_compatible("1.0.0", "1.0.0"));
assert!(is_compatible("1.0.0", "1.1.0"));
assert!(is_compatible("1.2.5", "1.0.0"));
assert!(!is_compatible("1.0.0", "2.0.0"));
assert!(!is_compatible("2.0.0", "1.0.0"));
}
#[test]
fn test_request_serialization() {
let req = RpcRequest::new(
RequestId::Number(1),
"tools/call",
Some(serde_json::json!({"name": "test"})),
);
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(json.contains("\"method\":\"tools/call\""));
}
}