Skip to main content

oxicode_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, Default, Serialize, Deserialize)]
16pub struct ServerEntry {
17    /// Command to start the MCP server process (stdio transport).
18    #[serde(default)]
19    pub command: Option<String>,
20    /// Arguments passed to the command.
21    #[serde(default)]
22    pub args: Option<Vec<String>>,
23    /// Additional environment variables for the server process.
24    #[serde(default)]
25    pub env: Option<HashMap<String, String>>,
26    /// Working directory for the server process.
27    #[serde(default)]
28    pub cwd: Option<String>,
29    /// HTTP URL for HTTP/SSE transport.
30    #[serde(default)]
31    pub url: Option<String>,
32    /// HTTP headers to include when connecting.
33    #[serde(default)]
34    pub headers: Option<HashMap<String, String>>,
35    /// Server lifecycle mode.
36    #[serde(default)]
37    pub lifecycle: Option<LifecycleMode>,
38    /// Idle timeout in minutes (overrides global setting).
39    #[serde(default, rename = "idleTimeout", alias = "idle_timeout")]
40    pub idle_timeout: Option<u64>,
41    /// Show server stderr output (default: false).
42    #[serde(default)]
43    pub debug: Option<bool>,
44    /// Direct tools registration config (Phase 3).
45    #[serde(default, rename = "directTools", alias = "direct_tools")]
46    pub direct_tools: Option<DirectToolsConfig>,
47    /// Tools to exclude from direct/proxy registration (Phase 3).
48    #[serde(default, rename = "excludeTools", alias = "exclude_tools")]
49    pub exclude_tools: Option<Vec<String>>,
50    /// Per-request timeout in milliseconds (`0` disables).
51    #[serde(default)]
52    pub timeout: Option<u64>,
53    /// OAuth2 client credentials for this server (v2.2). When present,
54    /// the credential provider performs a `client_credentials` grant
55    /// against `token_url` to obtain (and refresh) an `Authorization:
56    /// Bearer …` header for HTTP transports.
57    #[serde(default)]
58    pub oauth: Option<OAuthConfig>,
59}
60
61/// OAuth2 client credentials configuration for an MCP server.
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63pub struct OAuthConfig {
64    /// Token endpoint (e.g. `https://auth.example.com/oauth/token`).
65    #[serde(rename = "tokenUrl", alias = "token_url")]
66    pub token_url: String,
67    /// OAuth2 client id.
68    #[serde(rename = "clientId", alias = "client_id")]
69    pub client_id: String,
70    /// OAuth2 client secret.
71    #[serde(rename = "clientSecret", alias = "client_secret")]
72    pub client_secret: String,
73    /// Optional scope to request.
74    #[serde(default)]
75    pub scope: Option<String>,
76}
77/// Server lifecycle modes.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "kebab-case")]
80pub enum LifecycleMode {
81    /// Keep connection alive, auto-reconnect on failure.
82    KeepAlive,
83    /// Connect on first use, disconnect after idle timeout.
84    Lazy,
85    /// Connect eagerly at startup.
86    Eager,
87}
88
89/// Global MCP settings.
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
91pub struct McpSettings {
92    /// Tool name prefix mode.
93    #[serde(default, rename = "toolPrefix", alias = "tool_prefix")]
94    pub tool_prefix: Option<ToolPrefix>,
95    /// Global idle timeout in minutes (default: 10).
96    #[serde(default, rename = "idleTimeout", alias = "idle_timeout")]
97    pub idle_timeout: Option<u64>,
98    /// Back-off period in seconds after a server connection failure (default: 30).
99    #[serde(default, rename = "failureBackoffSecs", alias = "failure_backoff_secs")]
100    pub failure_backoff_secs: Option<u64>,
101    /// Global default for direct tools registration (Phase 3).
102    #[serde(default, rename = "directTools", alias = "direct_tools")]
103    pub direct_tools: Option<DirectToolsConfig>,
104    /// If true, the `mcp` proxy tool is hidden when direct tools cover
105    /// all configured servers (Phase 3).
106    #[serde(default, rename = "disableProxyTool", alias = "disable_proxy_tool")]
107    pub disable_proxy_tool: Option<bool>,
108    /// Opt-in: also read MCP server definitions from third-party tools
109    /// (`.claude/mcp.json`, `.cursor/mcp.json`, ...) under the current
110    /// project. Oxicode's own `.oxicode/mcp.json` always wins. Default: false
111    /// (preserves oxicode's identity; enable to ease adoption from
112    /// Claude/Cursor). See design §9.2 / G8.
113    #[serde(
114        default,
115        rename = "discoverExternalConfigs",
116        alias = "discover_external_configs"
117    )]
118    pub discover_external_configs: Option<bool>,
119}
120
121/// Tool name prefix strategy.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "kebab-case")]
124pub enum ToolPrefix {
125    /// `{server_name}_{tool_name}` (default).
126    Server,
127    /// No prefix.
128    None,
129    /// Short server name prefix.
130    Short,
131}
132
133/// Root MCP configuration.
134#[derive(Debug, Clone, Serialize, Deserialize, Default)]
135pub struct McpConfig {
136    /// Map of server name → server definition.
137    #[serde(rename = "mcpServers", alias = "mcp_servers")]
138    pub mcp_servers: HashMap<String, ServerEntry>,
139    /// Global settings override.
140    pub settings: Option<McpSettings>,
141}
142
143// ── MCP protocol types ───────────────────────────────────────────────
144
145/// Tool definition discovered from an MCP server.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct McpToolDef {
148    /// Tool name (unique within the server).
149    pub name: String,
150    /// Human-readable description.
151    pub description: Option<String>,
152    /// JSON Schema for the tool's input parameters.
153    pub input_schema: Option<serde_json::Value>,
154}
155
156/// Cached tool metadata with server association and naming.
157#[derive(Debug, Clone)]
158pub struct ToolMetadata {
159    /// Prefixed tool name (e.g. `my_server_list_files`).
160    pub name: String,
161    /// Original MCP tool name.
162    pub original_name: String,
163    /// Server that provides this tool.
164    pub server_name: String,
165    /// Human-readable description.
166    pub description: String,
167    /// JSON Schema for parameters.
168    pub input_schema: Option<serde_json::Value>,
169}
170
171/// Content types returned by MCP tool calls.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(tag = "type")]
174pub enum McpContent {
175    /// Text content.
176    #[serde(rename = "text")]
177    Text { text: String },
178    /// Image content (base64-encoded).
179    #[serde(rename = "image")]
180    Image {
181        data: String,
182        #[serde(default)]
183        mime_type: Option<String>,
184    },
185    /// Embedded resource content.
186    #[serde(rename = "resource")]
187    Resource { resource: ResourceContent },
188}
189
190/// Embedded resource returned by an MCP server.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ResourceContent {
193    pub uri: String,
194    pub text: Option<String>,
195    pub blob: Option<String>,
196}
197
198/// Server info returned from the MCP `initialize` handshake.
199#[derive(Debug, Clone)]
200pub struct ServerInfo {
201    pub name: String,
202    pub version: Option<String>,
203    pub protocol_version: String,
204}
205
206/// Connection status of an MCP server.
207#[derive(Debug, Clone)]
208pub enum ServerStatus {
209    /// Server is connected and ready.
210    Connected,
211    /// Connection failed with an error message.
212    Failed(String),
213    /// Server has not been connected yet.
214    NotConnected,
215}
216
217/// Result of an MCP tool call.
218#[derive(Debug, Clone)]
219pub struct McpCallResult {
220    /// Content blocks returned by the tool.
221    pub content: Vec<McpContent>,
222    /// Whether the tool reported an error.
223    pub is_error: bool,
224}
225
226// ── JSON-RPC protocol types ──────────────────────────────────────────
227
228/// JSON-RPC 2.0 request.
229#[derive(Debug, Clone, Serialize)]
230pub struct JsonRpcRequest {
231    pub jsonrpc: &'static str,
232    pub id: u64,
233    pub method: String,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub params: Option<serde_json::Value>,
236}
237
238/// JSON-RPC 2.0 notification (no response expected).
239#[derive(Debug, Clone, Serialize)]
240pub struct JsonRpcNotification {
241    pub jsonrpc: &'static str,
242    pub method: String,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub params: Option<serde_json::Value>,
245}
246
247/// Raw JSON-RPC message (can be request, response, or notification).
248#[derive(Debug, Clone, Deserialize)]
249pub struct RawJsonRpcMessage {
250    pub jsonrpc: String,
251    pub id: Option<u64>,
252
253    pub method: Option<String>,
254    pub result: Option<serde_json::Value>,
255    pub error: Option<JsonRpcError>,
256}
257
258/// JSON-RPC error object.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct JsonRpcError {
261    pub code: i64,
262    pub message: String,
263    #[serde(default)]
264    pub data: Option<serde_json::Value>,
265}
266
267// ── Naming helpers ───────────────────────────────────────────────────
268
269/// Get the prefix string for a server name.
270pub fn get_server_prefix(server_name: &str, mode: &ToolPrefix) -> String {
271    match mode {
272        ToolPrefix::None => String::new(),
273        ToolPrefix::Short => {
274            let short = server_name
275                .trim_end_matches("-mcp")
276                .trim_end_matches("_mcp")
277                .replace('-', "_");
278            if short.is_empty() {
279                "mcp".to_string()
280            } else {
281                short
282            }
283        }
284        ToolPrefix::Server => server_name.replace('-', "_"),
285    }
286}
287
288/// Format a tool name with server prefix.
289pub fn format_tool_name(tool_name: &str, server_name: &str, mode: &ToolPrefix) -> String {
290    let prefix = get_server_prefix(server_name, mode);
291    if prefix.is_empty() {
292        tool_name.to_string()
293    } else {
294        format!("{}_{}", prefix, tool_name)
295    }
296}
297
298/// Get the effective prefix mode from settings (defaults to Server).
299pub fn effective_prefix_mode(settings: Option<&McpSettings>) -> ToolPrefix {
300    settings
301        .and_then(|s| s.tool_prefix.clone())
302        .unwrap_or(ToolPrefix::Server)
303}
304
305/// Format a JSON Schema into a human-readable string.
306pub fn format_schema(schema: &serde_json::Value, indent: &str) -> String {
307    let s = match schema.as_object() {
308        Some(obj) => obj,
309        None => return format!("{indent}(no schema)"),
310    };
311
312    let schema_type = s.get("type").and_then(|t| t.as_str()).unwrap_or("");
313    let properties = s.get("properties").and_then(|p| p.as_object());
314    let required = s
315        .get("required")
316        .and_then(|r| r.as_array())
317        .map(|arr| {
318            arr.iter()
319                .filter_map(|v| v.as_str().map(String::from))
320                .collect::<Vec<_>>()
321        })
322        .unwrap_or_default();
323
324    if schema_type == "object"
325        && let Some(props) = properties
326    {
327        if props.is_empty() {
328            return format!("{indent}(no parameters)");
329        }
330        let mut lines = Vec::new();
331        for (name, prop_schema) in props {
332            let is_required = required.iter().any(|r| r == name);
333            let type_str = prop_schema
334                .get("type")
335                .and_then(|t| t.as_str())
336                .unwrap_or("any");
337            let desc = prop_schema
338                .get("description")
339                .and_then(|d| d.as_str())
340                .unwrap_or("");
341            let req_mark = if is_required { " *required*" } else { "" };
342            let desc_part = if desc.is_empty() {
343                String::new()
344            } else {
345                format!(" - {desc}")
346            };
347            lines.push(format!("{indent}{name} ({type_str}){req_mark}{desc_part}"));
348        }
349        return lines.join("\n");
350    }
351
352    format!("{indent}({schema_type})")
353}
354
355// ── Phase 3: Direct tools / consent ───────────────────────────────
356
357/// Configuration for direct tool registration (Phase 3).
358///
359/// Can be a boolean (all tools) or a list of specific tool names.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361#[serde(untagged)]
362pub enum DirectToolsConfig {
363    /// `true` = register all tools as direct, `false` = proxy only.
364    All(bool),
365    /// Register only these specific tools as direct (by original name).
366    Specific(Vec<String>),
367}
368
369impl Default for DirectToolsConfig {
370    fn default() -> Self {
371        DirectToolsConfig::All(false)
372    }
373}
374
375/// MCP consent state.
376///
377/// Two persisted states (`Allow` / `Deny`) survive across sessions in
378/// `mcp-consent.json`. The transient `Ask` variant is never persisted: it
379/// is the in-memory default for *unknown servers at the spawn gate* and is
380/// resolved to `Allow`/`Deny` (via `oxicode mcp trust`/`untrust`) before
381/// storage. The per-tool consent path ([`crate::mcp::consent::ConsentManager::check`])
382/// keeps `Allow` as its default so the tool-call gate at
383/// `direct_tool.rs` (`!= Deny`) is unaffected.
384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
385#[serde(rename_all = "lowercase")]
386pub enum ConsentState {
387    /// Persisted: always allow.
388    #[default]
389    Allow,
390    /// Persisted: always deny.
391    Deny,
392    /// Transient (never persisted): unknown at the spawn gate — resolve
393    /// via `ConsentManager::check_spawn_consent`. Serializes as `"ask"`
394    /// for forward compatibility but `ConsentManager::decide` never
395    /// stores it.
396    Ask,
397}
398
399/// Definition for a tool to be registered as a direct `AgentTool` (Phase 3).
400#[derive(Debug, Clone)]
401pub struct DirectToolDef {
402    /// Prefixed tool name (already computed at registration time so that
403    /// `AgentTool::name() -> &str` can return a reference into `self`).
404    pub prefixed_name: String,
405    /// Original (unprefixed) MCP tool name.
406    pub original_name: String,
407    /// Server that provides this tool.
408    pub server_name: String,
409    /// Tool description.
410    pub description: String,
411    /// JSON Schema for parameters.
412    pub input_schema: Option<serde_json::Value>,
413}
414
415// ── Phase 2: TUI dashboard data ────────────────────────────────────
416
417/// Connection status of an MCP server (Phase 2).
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum McpConnectionStatus {
420    /// Server is connected and ready.
421    Connected,
422    /// Server is configured but not connected (lazy).
423    Disconnected,
424    /// Connection attempt is in progress.
425    Connecting,
426    /// Connection failed with an error message.
427    Error(String),
428}
429
430/// One server's information for the TUI dashboard (Phase 2).
431#[derive(Debug, Clone)]
432pub struct McpServerInfo {
433    pub name: String,
434    pub status: McpConnectionStatus,
435    /// Human-readable lifecycle string ("lazy", "eager", "keep-alive", "none").
436    pub lifecycle: String,
437    /// Number of tools (cached or live).
438    pub tool_count: usize,
439    /// Per-tool information (empty if server is not connected and has no cache).
440    pub tools: Vec<McpToolInfo>,
441}
442
443/// One tool's information for the TUI dashboard (Phase 2).
444#[derive(Debug, Clone)]
445pub struct McpToolInfo {
446    /// Prefixed tool name.
447    pub name: String,
448    /// Original (unprefixed) tool name.
449    pub original_name: String,
450    pub description: String,
451    /// Whether this tool is registered as a direct `AgentTool` (Phase 3).
452    pub is_direct: bool,
453    /// Current consent state (Phase 3).
454    pub consent: ConsentState,
455}
456
457/// Settings summary for the dashboard header.
458#[derive(Debug, Clone)]
459pub struct McpSettingsView {
460    /// Current tool prefix mode as a string ("server", "short", "none").
461    pub tool_prefix: String,
462    /// Global idle timeout in minutes.
463    pub idle_timeout: Option<u64>,
464    /// Total number of configured servers.
465    pub total_servers: usize,
466    /// Number of currently connected servers.
467    pub connected_servers: usize,
468    /// Total number of known tools (across all servers).
469    pub total_tools: usize,
470}
471
472/// The full structured snapshot consumed by the TUI dashboard (Phase 2).
473#[derive(Debug, Clone)]
474pub struct McpDashboardData {
475    pub servers: Vec<McpServerInfo>,
476    pub settings: McpSettingsView,
477}