Skip to content

Commit 1d0549b

Browse files
committed
Update MCP to make it actually work
- Add Streamable HTTP support to be compatible with latest (2025-11-xx) MCP standards. - Move some tools that are really resources to resources. - Fix broken tools - It now works with Claude Code and MCP inspector, but not with Antigravity, nor Amp.
1 parent d25387d commit 1d0549b

14 files changed

Lines changed: 1365 additions & 152 deletions

File tree

.claude/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"mcpServers": {
3+
"cmdr": {
4+
"url": "http://localhost:9224/mcp"
5+
}
6+
}
7+
}

apps/desktop/src-tauri/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-feature
4747
# MCP server
4848
axum = "0.8"
4949
tokio = { version = "1", features = ["rt-multi-thread", "net", "time", "sync", "macros"] }
50+
futures-util = "0.3"
5051
tower-http = { version = "0.6", features = ["cors"] }
5152
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
5253
tauri-plugin-window-state = "2"
@@ -62,6 +63,7 @@ objc2-foundation = { version = "0.3", features = [
6263
"NSURL", "NSString", "NSDictionary", "NSDate", "NSArray", "NSValue", "NSError",
6364
"NSFileManager", "NSNetServices", "NSRunLoop"
6465
] }
66+
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSRunningApplication"] }
6567
smb = "0.11.1"
6668
smb-rpc = "=0.11.1"
6769
chrono = "0.4"

apps/desktop/src-tauri/src/mcp/executor.rs

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use tauri::{AppHandle, Emitter, Manager, Runtime};
88

99
use super::pane_state::PaneStateStore;
1010
use super::protocol::{INTERNAL_ERROR, INVALID_PARAMS};
11-
use crate::commands::ui::{set_view_mode, toggle_hidden_files};
11+
use crate::commands::ui::{
12+
copy_to_clipboard, get_info, quick_look, set_view_mode, show_in_finder, toggle_hidden_files,
13+
};
1214

1315
/// Result of tool execution.
1416
pub type ToolResult = Result<Value, ToolError>;
@@ -63,13 +65,20 @@ pub fn execute_tool<R: Runtime>(app: &AppHandle<R>, name: &str, params: &Value)
6365
fn execute_app_command<R: Runtime>(app: &AppHandle<R>, name: &str) -> ToolResult {
6466
match name {
6567
"app_quit" => {
66-
app.emit("mcp-action", json!({"action": "quit"}))
67-
.map_err(|e| ToolError::internal(e.to_string()))?;
68+
app.exit(0);
6869
Ok(json!({"success": true}))
6970
}
7071
"app_hide" => {
71-
app.emit("mcp-action", json!({"action": "hide"}))
72-
.map_err(|e| ToolError::internal(e.to_string()))?;
72+
// Use macOS NSApplication hide (same as ⌘H)
73+
#[cfg(target_os = "macos")]
74+
{
75+
use objc2::MainThreadMarker;
76+
use objc2_app_kit::NSApplication;
77+
if let Some(mtm) = MainThreadMarker::new() {
78+
let app_instance = NSApplication::sharedApplication(mtm);
79+
app_instance.hide(None);
80+
}
81+
}
7382
Ok(json!({"success": true}))
7483
}
7584
"app_about" => {
@@ -108,16 +117,6 @@ fn execute_pane_command<R: Runtime>(app: &AppHandle<R>, name: &str) -> ToolResul
108117
.map_err(|e| ToolError::internal(e.to_string()))?;
109118
Ok(json!({"success": true}))
110119
}
111-
"pane_leftVolumeChooser" => {
112-
app.emit("mcp-action", json!({"action": "leftVolumeChooser"}))
113-
.map_err(|e| ToolError::internal(e.to_string()))?;
114-
Ok(json!({"success": true}))
115-
}
116-
"pane_rightVolumeChooser" => {
117-
app.emit("mcp-action", json!({"action": "rightVolumeChooser"}))
118-
.map_err(|e| ToolError::internal(e.to_string()))?;
119-
Ok(json!({"success": true}))
120-
}
121120
_ => Err(ToolError::invalid_params(format!("Unknown pane command: {name}"))),
122121
}
123122
}
@@ -166,31 +165,54 @@ fn execute_sort_command<R: Runtime>(app: &AppHandle<R>, name: &str) -> ToolResul
166165
}
167166

168167
/// Execute a file command.
168+
/// Gets the selected file from PaneStateStore and operates on it directly.
169169
fn execute_file_command<R: Runtime>(app: &AppHandle<R>, name: &str) -> ToolResult {
170-
let action = match name {
171-
"file_showInFinder" => "showInFinder",
172-
"file_copyPath" => "copyPath",
173-
"file_copyFilename" => "copyFilename",
174-
"file_quickLook" => "quickLook",
175-
"file_getInfo" => "getInfo",
176-
_ => return Err(ToolError::invalid_params(format!("Unknown file command: {name}"))),
170+
// Get the selected file from pane state
171+
let store = app
172+
.try_state::<PaneStateStore>()
173+
.ok_or_else(|| ToolError::internal("Pane state not initialized"))?;
174+
175+
let focused = store.get_focused_pane();
176+
let pane = if focused == "right" {
177+
store.get_right()
178+
} else {
179+
store.get_left()
177180
};
178181

179-
app.emit("mcp-action", json!({"action": action, "type": "file"}))
180-
.map_err(|e| ToolError::internal(e.to_string()))?;
181-
Ok(json!({"success": true}))
182+
let selected = pane
183+
.files
184+
.get(pane.selected_index)
185+
.ok_or_else(|| ToolError::internal("No file selected"))?;
186+
187+
match name {
188+
"file_showInFinder" => {
189+
show_in_finder(selected.path.clone()).map_err(ToolError::internal)?;
190+
Ok(json!({"success": true, "path": selected.path}))
191+
}
192+
"file_copyPath" => {
193+
copy_to_clipboard(app.clone(), selected.path.clone()).map_err(ToolError::internal)?;
194+
Ok(json!({"success": true, "copied": selected.path}))
195+
}
196+
"file_copyFilename" => {
197+
copy_to_clipboard(app.clone(), selected.name.clone()).map_err(ToolError::internal)?;
198+
Ok(json!({"success": true, "copied": selected.name}))
199+
}
200+
"file_quickLook" => {
201+
quick_look(selected.path.clone()).map_err(ToolError::internal)?;
202+
Ok(json!({"success": true, "path": selected.path}))
203+
}
204+
"file_getInfo" => {
205+
get_info(selected.path.clone()).map_err(ToolError::internal)?;
206+
Ok(json!({"success": true, "path": selected.path}))
207+
}
208+
_ => Err(ToolError::invalid_params(format!("Unknown file command: {name}"))),
209+
}
182210
}
183211

184212
/// Execute a volume command.
213+
/// Note: volume listing is now a resource (cmdr://volumes), not a tool.
185214
fn execute_volume_command<R: Runtime>(app: &AppHandle<R>, name: &str, params: &Value) -> ToolResult {
186215
match name {
187-
"volume_list" => {
188-
// Request volume list from frontend - it will respond via event
189-
app.emit("mcp-volume-list-request", ())
190-
.map_err(|e| ToolError::internal(e.to_string()))?;
191-
// For now, return a placeholder - real implementation needs async response
192-
Ok(json!({"note": "Volume list requested, check mcp-volume-list-response event"}))
193-
}
194216
"volume_selectLeft" | "volume_selectRight" => {
195217
let index = params
196218
.get("index")

apps/desktop/src-tauri/src/mcp/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
//! MCP (Model Context Protocol) server module.
22
//!
3-
//! Provides an HTTP+SSE server that exposes cmdr functionality as MCP tools,
3+
//! Provides a Streamable HTTP server that exposes cmdr functionality as MCP tools,
44
//! enabling AI agents to control the file manager.
55
66
mod config;
77
mod executor;
88
pub mod pane_state;
99
mod protocol;
10+
mod resources;
1011
mod server;
1112
mod tools;
1213

apps/desktop/src-tauri/src/mcp/protocol.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
//! MCP protocol message types and handling.
1+
//! MCP protocol message types and handling (spec version 2025-11-25).
2+
//!
3+
//! Implements JSON-RPC 2.0 message format as required by MCP.
4+
//! See: https://modelcontextprotocol.io/specification/2025-11-25
25
36
use serde::{Deserialize, Serialize};
47
use serde_json::Value;
@@ -35,11 +38,9 @@ pub struct McpError {
3538
pub data: Option<Value>,
3639
}
3740

38-
// MCP error codes (JSON-RPC standard + MCP specific)
39-
// Some are reserved for future use
41+
// JSON-RPC 2.0 standard error codes
4042
#[allow(dead_code)]
4143
pub const PARSE_ERROR: i32 = -32700;
42-
#[allow(dead_code)]
4344
pub const INVALID_REQUEST: i32 = -32600;
4445
pub const METHOD_NOT_FOUND: i32 = -32601;
4546
pub const INVALID_PARAMS: i32 = -32602;
@@ -98,6 +99,7 @@ pub struct ServerCapabilities {
9899
#[derive(Debug, Clone, Serialize)]
99100
pub struct Capabilities {
100101
pub tools: ToolCapabilities,
102+
pub resources: ResourceCapabilities,
101103
}
102104

103105
#[derive(Debug, Clone, Serialize)]
@@ -106,6 +108,13 @@ pub struct ToolCapabilities {
106108
pub list_changed: bool,
107109
}
108110

111+
#[derive(Debug, Clone, Serialize)]
112+
pub struct ResourceCapabilities {
113+
#[serde(rename = "listChanged")]
114+
pub list_changed: bool,
115+
pub subscribe: bool,
116+
}
117+
109118
#[derive(Debug, Clone, Serialize)]
110119
pub struct ServerInfo {
111120
pub name: String,
@@ -118,6 +127,10 @@ impl Default for ServerCapabilities {
118127
protocol_version: "2024-11-05".to_string(),
119128
capabilities: Capabilities {
120129
tools: ToolCapabilities { list_changed: false },
130+
resources: ResourceCapabilities {
131+
list_changed: false,
132+
subscribe: false,
133+
},
121134
},
122135
server_info: ServerInfo {
123136
name: "cmdr".to_string(),
@@ -188,4 +201,17 @@ mod tests {
188201
assert_eq!(caps.server_info.name, "cmdr");
189202
assert!(!caps.capabilities.tools.list_changed);
190203
}
204+
205+
#[test]
206+
fn test_error_with_data() {
207+
let response = McpResponse::error_with_data(
208+
Some(json!(1)),
209+
INVALID_PARAMS,
210+
"Invalid params",
211+
json!({"details": "missing field"}),
212+
);
213+
let json = serde_json::to_value(&response).unwrap();
214+
215+
assert_eq!(json["error"]["data"]["details"], "missing field");
216+
}
191217
}

0 commit comments

Comments
 (0)