use std::collections::HashSet;
use std::io::{self, BufRead, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::Deserialize;
use serde_json::{Value, json};
use tokio::runtime::Runtime;
use crate::session_manager::SessionManager;
use crate::tools::spec::{ToolError, ToolResult};
use crate::tools::{ToolContext, ToolRegistryBuilder};
#[derive(Debug, Default, Deserialize)]
struct McpServerConfigFile {
#[serde(default)]
server: McpServerSection,
}
#[derive(Debug, Default, Deserialize)]
struct McpServerSection {
expose_tools: Option<Vec<String>>,
require_approval: Option<bool>,
}
#[derive(Debug, Clone)]
struct McpServerSettings {
expose_tools: Vec<String>,
require_approval: bool,
}
impl McpServerSettings {
fn load() -> Result<Self> {
let path = default_config_path();
if let Some(path) = path.filter(|p| p.exists()) {
let contents = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read MCP server config: {}", path.display()))?;
let config: McpServerConfigFile = toml::from_str(&contents).with_context(|| {
format!("Failed to parse MCP server config: {}", path.display())
})?;
let expose_tools = config
.server
.expose_tools
.unwrap_or_else(default_expose_tools);
let require_approval = config.server.require_approval.unwrap_or(false);
Ok(Self {
expose_tools,
require_approval,
})
} else {
Ok(Self {
expose_tools: default_expose_tools(),
require_approval: false,
})
}
}
}
#[derive(Debug, Clone)]
struct ExposedTool {
public: String,
internal: String,
}
pub fn run_mcp_server(workspace: PathBuf) -> Result<()> {
let settings = McpServerSettings::load()?;
let mut server = McpServer::new(workspace, settings)?;
server.run()
}
struct McpServer {
workspace: PathBuf,
registry: crate::tools::ToolRegistry,
exposed_tools: Vec<ExposedTool>,
require_approval: bool,
}
impl McpServer {
fn new(workspace: PathBuf, settings: McpServerSettings) -> Result<Self> {
let exposed_tools = build_exposed_tools(&settings.expose_tools);
let mut internal_names: HashSet<String> = HashSet::new();
for tool in &exposed_tools {
internal_names.insert(tool.internal.clone());
}
let mut builder = ToolRegistryBuilder::new()
.with_file_tools()
.with_search_tools();
if internal_names.contains("apply_patch") {
builder = builder.with_patch_tools();
}
if internal_names.contains("exec_shell") {
builder = builder.with_shell_tools();
}
let context = ToolContext::new(workspace.clone());
let registry = builder.build(context);
Ok(Self {
workspace,
registry,
exposed_tools,
require_approval: settings.require_approval,
})
}
fn run(&mut self) -> Result<()> {
let runtime = Runtime::new().context("Failed to start MCP runtime")?;
let stdin = io::stdin();
let mut stdout = io::stdout();
for line in stdin.lock().lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let Ok(message) = serde_json::from_str::<Value>(trimmed) else {
continue;
};
if let Some(response) = self.handle_message(&runtime, message) {
let payload = serde_json::to_string(&response)?;
writeln!(stdout, "{payload}")?;
stdout.flush()?;
}
}
Ok(())
}
fn handle_message(&mut self, runtime: &Runtime, message: Value) -> Option<Value> {
let method = message.get("method").and_then(Value::as_str)?;
let id = message.get("id").cloned();
match method {
"initialize" => respond(id.as_ref(), initialize_response()),
"tools/list" => respond(id.as_ref(), self.list_tools_response()),
"tools/call" => {
let params = message.get("params").cloned().unwrap_or_else(|| json!({}));
match self.call_tool(runtime, params) {
Ok(result) => respond(id.as_ref(), result),
Err(err) => respond_error(id.as_ref(), err.code, err.message),
}
}
"resources/list" => respond(id.as_ref(), self.list_resources_response()),
"ping" => respond(id.as_ref(), json!({})),
"notifications/initialized" => None,
_ => respond_error(id.as_ref(), -32601, format!("Method not found: {method}")),
}
}
fn list_tools_response(&self) -> Value {
let mut tools = Vec::new();
let mut seen = HashSet::new();
for entry in &self.exposed_tools {
if !seen.insert(entry.public.clone()) {
continue;
}
if let Some(tool) = self.registry.get(&entry.internal) {
tools.push(json!({
"name": entry.public,
"description": tool.description(),
"inputSchema": tool.input_schema(),
}));
}
}
json!({ "tools": tools, "nextCursor": Value::Null })
}
fn list_resources_response(&self) -> Value {
let mut resources = Vec::new();
resources.push(json!({
"uri": format!("file://{}", self.workspace.display()),
"name": "workspace",
"description": "Workspace root",
"mimeType": "inode/directory",
}));
if let Ok(manager) = SessionManager::default_location()
&& let Ok(sessions) = manager.list_sessions()
{
for session in sessions {
resources.push(json!({
"uri": format!("deepseek://session/{}", session.id),
"name": session.title,
"description": format!("{} messages", session.message_count),
"mimeType": "application/json",
}));
}
}
json!({ "resources": resources, "nextCursor": Value::Null })
}
fn call_tool(&self, runtime: &Runtime, params: Value) -> Result<Value, RpcError> {
let params = params.as_object().ok_or_else(|| RpcError {
code: -32602,
message: "Invalid params for tools/call".to_string(),
})?;
let name = params
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| RpcError {
code: -32602,
message: "Missing tool name".to_string(),
})?;
if self.require_approval
&& !params
.get("approved")
.and_then(Value::as_bool)
.unwrap_or(false)
{
return Err(RpcError {
code: -32001,
message: "Approval required. Resend with approved=true.".to_string(),
});
}
let internal = self
.exposed_tools
.iter()
.find(|tool| tool.public == name)
.map(|tool| tool.internal.clone())
.ok_or_else(|| RpcError {
code: -32602,
message: format!("Tool not exposed: {name}"),
})?;
let arguments = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
let result = runtime.block_on(self.registry.execute_full(&internal, arguments));
Ok(tool_result_to_mcp(result))
}
}
fn default_config_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".deepseek").join("mcp_server.toml"))
}
fn default_expose_tools() -> Vec<String> {
vec![
"file_read".to_string(),
"file_write".to_string(),
"search".to_string(),
"apply_patch".to_string(),
"shell".to_string(),
]
}
fn build_exposed_tools(names: &[String]) -> Vec<ExposedTool> {
let mut tools = Vec::new();
for name in names {
let trimmed = name.trim();
if trimmed.is_empty() {
continue;
}
let public = trimmed.to_string();
let internal = match trimmed {
"file_read" => "read_file",
"file_write" => "write_file",
"file_edit" => "edit_file",
"shell" => "exec_shell",
"search" => "grep_files",
"file_search" => "file_search",
other => other,
}
.to_string();
tools.push(ExposedTool { public, internal });
}
tools
}
fn tool_result_to_mcp(result: Result<ToolResult, ToolError>) -> Value {
match result {
Ok(tool_result) => {
let mut response = json!({
"content": [{ "type": "text", "text": tool_result.content }],
"isError": !tool_result.success,
});
if let Some(metadata) = tool_result.metadata {
response["structuredContent"] = metadata;
}
response
}
Err(err) => json!({
"content": [{ "type": "text", "text": err.to_string() }],
"isError": true,
}),
}
}
fn initialize_response() -> Value {
json!({
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "deepseek-mcp-server",
"version": env!("CARGO_PKG_VERSION"),
},
"capabilities": {
"tools": {},
"resources": {},
}
})
}
fn respond(id: Option<&Value>, result: Value) -> Option<Value> {
id.map(|id| json!({ "jsonrpc": "2.0", "id": id, "result": result }))
}
fn respond_error(id: Option<&Value>, code: i64, message: String) -> Option<Value> {
id.map(|id| {
json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": code, "message": message }
})
})
}
#[derive(Debug)]
struct RpcError {
code: i64,
message: String,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn exposed_tools_map_aliases() {
let names = vec![
"file_read".to_string(),
"file_write".to_string(),
"search".to_string(),
"apply_patch".to_string(),
"shell".to_string(),
];
let tools = build_exposed_tools(&names);
let mut map = HashMap::new();
for tool in tools {
map.insert(tool.public, tool.internal);
}
assert_eq!(map.get("file_read").map(String::as_str), Some("read_file"));
assert_eq!(
map.get("file_write").map(String::as_str),
Some("write_file")
);
assert_eq!(map.get("search").map(String::as_str), Some("grep_files"));
assert_eq!(
map.get("apply_patch").map(String::as_str),
Some("apply_patch")
);
assert_eq!(map.get("shell").map(String::as_str), Some("exec_shell"));
}
}