Skip to main content

oxi_agent/mcp/
types.rs

1//! Config format
2//! ...
3
4#![allow(missing_docs)]
5#![allow(clippy::unwrap_used)]
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10// ── Configuration types ──────────────────────────────────────────────
11
12/// MCP server configuration entry.
13///
14/// Supports both stdio (command-based) and HTTP transports.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ServerEntry {
17    /// Command to start the MCP server process (stdio transport).
18    pub command: Option<String>,
19    /// Arguments passed to the command.
20    pub args: Option<Vec<String>>,
21    /// Additional environment variables for the server process.
22    pub env: Option<HashMap<String, String>>,
23    /// Working directory for the server process.
24    pub cwd: Option<String>,
25    /// HTTP URL for HTTP/SSE transport.
26    pub url: Option<String>,
27    /// HTTP headers to include when connecting.
28    pub headers: Option<HashMap<String, String>>,
29    /// Server lifecycle mode.
30    pub lifecycle: Option<LifecycleMode>,
31    /// Idle timeout in minutes (overrides global setting).
32    pub idle_timeout: Option<u64>,
33    /// Show server stderr output (default: false).
34    pub debug: Option<bool>,
35}
36
37/// Server lifecycle modes.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum LifecycleMode {
41    /// Keep connection alive, auto-reconnect on failure.
42    KeepAlive,
43    /// Connect on first use, disconnect after idle timeout.
44    Lazy,
45    /// Connect eagerly at startup.
46    Eager,
47}
48
49/// Global MCP settings.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct McpSettings {
52    /// Tool name prefix mode.
53    pub tool_prefix: Option<ToolPrefix>,
54    /// Global idle timeout in minutes (default: 10).
55    pub idle_timeout: Option<u64>,
56    /// Back-off period in seconds after a server connection failure (default: 30).
57    pub failure_backoff_secs: Option<u64>,
58}
59
60/// Tool name prefix strategy.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "kebab-case")]
63pub enum ToolPrefix {
64    /// `{server_name}_{tool_name}` (default).
65    Server,
66    /// No prefix.
67    None,
68    /// Short server name prefix.
69    Short,
70}
71
72/// Root MCP configuration.
73#[derive(Debug, Clone, Serialize, Deserialize, Default)]
74pub struct McpConfig {
75    /// Map of server name → server definition.
76    pub mcp_servers: HashMap<String, ServerEntry>,
77    /// Global settings override.
78    pub settings: Option<McpSettings>,
79}
80
81// ── MCP protocol types ───────────────────────────────────────────────
82
83/// Tool definition discovered from an MCP server.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct McpToolDef {
86    /// Tool name (unique within the server).
87    pub name: String,
88    /// Human-readable description.
89    pub description: Option<String>,
90    /// JSON Schema for the tool's input parameters.
91    pub input_schema: Option<serde_json::Value>,
92}
93
94/// Cached tool metadata with server association and naming.
95#[derive(Debug, Clone)]
96pub struct ToolMetadata {
97    /// Prefixed tool name (e.g. `my_server_list_files`).
98    pub name: String,
99    /// Original MCP tool name.
100    pub original_name: String,
101    /// Server that provides this tool.
102    pub server_name: String,
103    /// Human-readable description.
104    pub description: String,
105    /// JSON Schema for parameters.
106    pub input_schema: Option<serde_json::Value>,
107}
108
109/// Content types returned by MCP tool calls.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "type")]
112pub enum McpContent {
113    /// Text content.
114    #[serde(rename = "text")]
115    Text { text: String },
116    /// Image content (base64-encoded).
117    #[serde(rename = "image")]
118    Image {
119        data: String,
120        #[serde(default)]
121        mime_type: Option<String>,
122    },
123    /// Embedded resource content.
124    #[serde(rename = "resource")]
125    Resource { resource: ResourceContent },
126}
127
128/// Embedded resource returned by an MCP server.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ResourceContent {
131    pub uri: String,
132    pub text: Option<String>,
133    pub blob: Option<String>,
134}
135
136/// Server info returned from the MCP `initialize` handshake.
137#[derive(Debug, Clone)]
138pub struct ServerInfo {
139    pub name: String,
140    pub version: Option<String>,
141    pub protocol_version: String,
142}
143
144/// Connection status of an MCP server.
145#[derive(Debug, Clone)]
146pub enum ServerStatus {
147    /// Server is connected and ready.
148    Connected,
149    /// Connection failed with an error message.
150    Failed(String),
151    /// Server has not been connected yet.
152    NotConnected,
153}
154
155/// Result of an MCP tool call.
156#[derive(Debug, Clone)]
157pub struct McpCallResult {
158    /// Content blocks returned by the tool.
159    pub content: Vec<McpContent>,
160    /// Whether the tool reported an error.
161    pub is_error: bool,
162}
163
164// ── JSON-RPC protocol types ──────────────────────────────────────────
165
166/// JSON-RPC 2.0 request.
167#[derive(Debug, Clone, Serialize)]
168pub struct JsonRpcRequest {
169    pub jsonrpc: &'static str,
170    pub id: u64,
171    pub method: String,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub params: Option<serde_json::Value>,
174}
175
176/// JSON-RPC 2.0 notification (no response expected).
177#[derive(Debug, Clone, Serialize)]
178pub struct JsonRpcNotification {
179    pub jsonrpc: &'static str,
180    pub method: String,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub params: Option<serde_json::Value>,
183}
184
185/// Raw JSON-RPC message (can be request, response, or notification).
186#[derive(Debug, Clone, Deserialize)]
187pub struct RawJsonRpcMessage {
188    pub jsonrpc: String,
189    pub id: Option<u64>,
190
191    pub method: Option<String>,
192    pub result: Option<serde_json::Value>,
193    pub error: Option<JsonRpcError>,
194}
195
196/// JSON-RPC error object.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct JsonRpcError {
199    pub code: i64,
200    pub message: String,
201    #[serde(default)]
202    pub data: Option<serde_json::Value>,
203}
204
205// ── Naming helpers ───────────────────────────────────────────────────
206
207/// Get the prefix string for a server name.
208pub fn get_server_prefix(server_name: &str, mode: &ToolPrefix) -> String {
209    match mode {
210        ToolPrefix::None => String::new(),
211        ToolPrefix::Short => {
212            let short = server_name
213                .trim_end_matches("-mcp")
214                .trim_end_matches("_mcp")
215                .replace('-', "_");
216            if short.is_empty() {
217                "mcp".to_string()
218            } else {
219                short
220            }
221        }
222        ToolPrefix::Server => server_name.replace('-', "_"),
223    }
224}
225
226/// Format a tool name with server prefix.
227pub fn format_tool_name(tool_name: &str, server_name: &str, mode: &ToolPrefix) -> String {
228    let prefix = get_server_prefix(server_name, mode);
229    if prefix.is_empty() {
230        tool_name.to_string()
231    } else {
232        format!("{}_{}", prefix, tool_name)
233    }
234}
235
236/// Get the effective prefix mode from settings (defaults to Server).
237pub fn effective_prefix_mode(settings: Option<&McpSettings>) -> ToolPrefix {
238    settings
239        .and_then(|s| s.tool_prefix.clone())
240        .unwrap_or(ToolPrefix::Server)
241}
242
243/// Format a JSON Schema into a human-readable string.
244pub fn format_schema(schema: &serde_json::Value, indent: &str) -> String {
245    let s = match schema.as_object() {
246        Some(obj) => obj,
247        None => return format!("{indent}(no schema)"),
248    };
249
250    let schema_type = s.get("type").and_then(|t| t.as_str()).unwrap_or("");
251    let properties = s.get("properties").and_then(|p| p.as_object());
252    let required = s
253        .get("required")
254        .and_then(|r| r.as_array())
255        .map(|arr| {
256            arr.iter()
257                .filter_map(|v| v.as_str().map(String::from))
258                .collect::<Vec<_>>()
259        })
260        .unwrap_or_default();
261
262    if schema_type == "object" {
263        if let Some(props) = properties {
264            if props.is_empty() {
265                return format!("{indent}(no parameters)");
266            }
267            let mut lines = Vec::new();
268            for (name, prop_schema) in props {
269                let is_required = required.iter().any(|r| r == name);
270                let type_str = prop_schema
271                    .get("type")
272                    .and_then(|t| t.as_str())
273                    .unwrap_or("any");
274                let desc = prop_schema
275                    .get("description")
276                    .and_then(|d| d.as_str())
277                    .unwrap_or("");
278                let req_mark = if is_required { " *required*" } else { "" };
279                let desc_part = if desc.is_empty() {
280                    String::new()
281                } else {
282                    format!(" - {desc}")
283                };
284                lines.push(format!("{indent}{name} ({type_str}){req_mark}{desc_part}"));
285            }
286            return lines.join("\n");
287        }
288    }
289
290    format!("{indent}({schema_type})")
291}