- 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`.
390 lines
11 KiB
Rust
390 lines
11 KiB
Rust
/// 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,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// JSON‑RPC 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\""));
|
||
}
|
||
}
|