use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::sync::{mpsc, RwLock, Semaphore};
use tracing::{debug, info};
use crate::safety::SafetyChecker;
use crate::tools::ToolRegistry;
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: &'static str,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl std::fmt::Display for JsonRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JSON-RPC error {}: {}", self.code, self.message)
}
}
const PARSE_ERROR: i64 = -32700;
const INVALID_REQUEST: i64 = -32600;
const METHOD_NOT_FOUND: i64 = -32601;
const INVALID_PARAMS: i64 = -32602;
const INTERNAL_ERROR: i64 = -32603;
const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
const SERVER_NAME: &str = "selfware";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Framing {
ContentLength,
NewlineDelimited,
}
async fn detect_framing<R: tokio::io::AsyncRead + Unpin>(
reader: &mut BufReader<R>,
) -> Result<Option<Framing>> {
let buf = reader.fill_buf().await?;
if buf.is_empty() {
return Ok(None);
}
Ok(Some(if buf[0] == b'C' {
Framing::ContentLength
} else {
Framing::NewlineDelimited
}))
}
#[cfg(test)]
async fn read_message<R: tokio::io::AsyncRead + Unpin>(
reader: &mut BufReader<R>,
) -> Result<Option<(String, Framing)>> {
let framing = match detect_framing(reader).await? {
Some(framing) => framing,
None => return Ok(None),
};
let body = match framing {
Framing::ContentLength => read_content_length_message(reader).await?,
Framing::NewlineDelimited => read_newline_message(reader).await?,
};
Ok(body.map(|body| (body, framing)))
}
async fn read_content_length_message<R: tokio::io::AsyncRead + Unpin>(
reader: &mut BufReader<R>,
) -> Result<Option<String>> {
let mut content_length: Option<usize> = None;
loop {
let mut header_line = String::new();
let bytes_read = reader.read_line(&mut header_line).await?;
if bytes_read == 0 {
return Ok(None);
}
let trimmed = header_line.trim();
if trimmed.is_empty() {
break;
}
if let Some(value) = trimmed.strip_prefix("Content-Length:") {
content_length = Some(
value
.trim()
.parse::<usize>()
.context("Invalid Content-Length value")?,
);
}
}
let length = content_length.context("Missing Content-Length header")?;
let mut buf = vec![0u8; length];
reader.read_exact(&mut buf).await?;
String::from_utf8(buf)
.context("Message body is not valid UTF-8")
.map(Some)
}
async fn read_newline_message<R: tokio::io::AsyncRead + Unpin>(
reader: &mut BufReader<R>,
) -> Result<Option<String>> {
let mut line = String::new();
let bytes_read = reader.read_line(&mut line).await?;
if bytes_read == 0 {
return Ok(None);
}
Ok(Some(line.trim().to_string()))
}
async fn write_message<W: tokio::io::AsyncWrite + Unpin>(writer: &mut W, body: &str) -> Result<()> {
let header = format!("Content-Length: {}\r\n\r\n", body.len());
writer.write_all(header.as_bytes()).await?;
writer.write_all(body.as_bytes()).await?;
writer.flush().await?;
Ok(())
}
async fn write_framed_message<W: tokio::io::AsyncWrite + Unpin>(
writer: &mut W,
body: &str,
framing: Framing,
) -> Result<()> {
match framing {
Framing::ContentLength => write_message(writer, body).await,
Framing::NewlineDelimited => {
writer.write_all(body.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
Ok(())
}
}
}
pub struct McpServer {
registry: Arc<RwLock<ToolRegistry>>,
project_root: PathBuf,
initialized: Arc<AtomicBool>,
safety: SafetyChecker,
config_path: Option<String>,
_project_root_env: PathBuf,
}
impl Default for McpServer {
fn default() -> Self {
Self::new()
}
}
fn load_mcp_safety_config(config_path: Option<&str>) -> crate::config::SafetyConfig {
match crate::config::Config::load(config_path) {
Ok(config) => config.safety,
Err(e) => {
tracing::warn!(
"MCP server: failed to load selfware config ({}), using default safety settings",
e
);
crate::config::SafetyConfig::default()
}
}
}
fn mcp_destructive_tools_allowed() -> bool {
std::env::var("SELFWARE_MCP_ALLOW_DESTRUCTIVE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
impl McpServer {
pub fn new() -> Self {
Self::with_config(None)
}
pub fn with_project_root(project_root: PathBuf) -> Self {
let project_root_env = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self {
registry: Arc::new(RwLock::new(ToolRegistry::new())),
project_root,
initialized: Arc::new(AtomicBool::new(false)),
safety: SafetyChecker::new(&load_mcp_safety_config(None)),
config_path: None,
_project_root_env: project_root_env,
}
}
pub fn with_config(config_path: Option<String>) -> Self {
let project_root = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self {
registry: Arc::new(RwLock::new(ToolRegistry::new())),
project_root,
initialized: Arc::new(AtomicBool::new(false)),
safety: SafetyChecker::new(&load_mcp_safety_config(config_path.as_deref())),
config_path,
_project_root_env: PathBuf::from("."),
}
}
pub fn with_explicit_safety_config(safety: crate::config::SafetyConfig) -> Self {
Self {
registry: Arc::new(RwLock::new(ToolRegistry::new())),
project_root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
initialized: Arc::new(AtomicBool::new(false)),
safety: SafetyChecker::new(&safety),
config_path: None,
_project_root_env: PathBuf::from("."),
}
}
pub async fn handle_request(&self, request: &JsonRpcRequest) -> Option<JsonRpcResponse> {
let id = match &request.id {
Some(id) => id.clone(),
None => {
self.handle_notification(request).await;
return None;
}
};
const PRE_INIT_METHODS: &[&str] = &["initialize", "ping", "shutdown"];
if !self.initialized.load(Ordering::SeqCst)
&& !PRE_INIT_METHODS.contains(&request.method.as_str())
{
return Some(JsonRpcResponse {
jsonrpc: "2.0",
id,
result: None,
error: Some(JsonRpcError {
code: INVALID_REQUEST,
message: format!(
"Method '{}' requires an 'initialize' request first",
request.method
),
data: None,
}),
});
}
let (result, error) = match request.method.as_str() {
"initialize" => self.handle_initialize(&request.params),
"tools/list" => self.handle_tools_list(&request.params).await,
"tools/call" => self.handle_tools_call(&request.params).await,
"resources/list" => self.handle_resources_list(&request.params),
"resources/read" => self.handle_resources_read(&request.params).await,
"ping" => (Some(serde_json::json!({})), None),
"shutdown" => {
info!("MCP server received shutdown request");
(Some(serde_json::json!({})), None)
}
_ => (
None,
Some(JsonRpcError {
code: METHOD_NOT_FOUND,
message: format!("Method not found: {}", request.method),
data: None,
}),
),
};
Some(JsonRpcResponse {
jsonrpc: "2.0",
id,
result,
error,
})
}
async fn handle_notification(&self, request: &JsonRpcRequest) {
match request.method.as_str() {
"notifications/initialized" => {
info!("MCP client confirmed initialization");
}
"notifications/cancelled" => {
if let Some(params) = &request.params {
let request_id = params.get("requestId");
debug!("Client cancelled request: {:?}", request_id);
}
}
"notifications/exit" => {
info!("MCP client sent exit notification");
}
_ => {
debug!("Unhandled notification: {}", request.method);
}
}
}
fn handle_initialize(&self, params: &Option<Value>) -> (Option<Value>, Option<JsonRpcError>) {
if let Some(p) = params {
if let Some(client_version) = p.get("protocolVersion").and_then(|v| v.as_str()) {
if client_version != MCP_PROTOCOL_VERSION {
tracing::warn!(
"MCP client requested protocol version '{}', we support '{}'",
client_version,
MCP_PROTOCOL_VERSION
);
}
}
}
self.initialized.store(true, Ordering::SeqCst);
let result = serde_json::json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"capabilities": {
"tools": {},
"resources": {
"subscribe": false,
"listChanged": false
}
},
"serverInfo": {
"name": SERVER_NAME,
"version": SERVER_VERSION
}
});
info!("MCP server initialized (protocol {})", MCP_PROTOCOL_VERSION);
(Some(result), None)
}
async fn handle_tools_list(
&self,
_params: &Option<Value>,
) -> (Option<Value>, Option<JsonRpcError>) {
let registry = self.registry.read().await;
let tools: Vec<Value> = registry
.list()
.iter()
.map(|tool| {
serde_json::json!({
"name": tool.name(),
"description": tool.description(),
"inputSchema": tool.schema()
})
})
.collect();
debug!("tools/list returning {} tools", tools.len());
(Some(serde_json::json!({ "tools": tools })), None)
}
async fn handle_tools_call(
&self,
params: &Option<Value>,
) -> (Option<Value>, Option<JsonRpcError>) {
let params = match params {
Some(p) => p,
None => {
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: "Missing params for tools/call".to_string(),
data: None,
}),
);
}
};
let tool_name = match params.get("name").and_then(|n| n.as_str()) {
Some(name) => name,
None => {
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: "Missing 'name' parameter in tools/call".to_string(),
data: None,
}),
);
}
};
let arguments = params
.get("arguments")
.cloned()
.unwrap_or(serde_json::json!({}));
debug!("tools/call: {} with args: {}", tool_name, arguments);
{
let registry = self.registry.read().await;
if let Some(tool) = registry.get(tool_name) {
let schema = tool.schema();
if schema.is_object() {
if let Err(e) =
crate::tools::validate_tool_arguments_schema(tool_name, &schema, &arguments)
{
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: e.to_string(),
data: Some(serde_json::json!({
"tool": tool_name,
"arguments": arguments,
})),
}),
);
}
}
}
}
let fake_call = crate::api::types::ToolCall {
id: "mcp".to_string(),
call_type: "function".to_string(),
function: crate::api::types::ToolFunction {
name: tool_name.to_string(),
arguments: arguments.to_string(),
},
};
if let Err(e) = self.safety.check_tool_call(&fake_call) {
let response = serde_json::json!({
"content": [
{
"type": "text",
"text": format!("Blocked by safety checker: {}", e)
}
],
"isError": true
});
return (Some(response), None);
}
if !mcp_destructive_tools_allowed() {
let is_destructive = {
let registry = self.registry.read().await;
registry
.get(tool_name)
.is_some_and(|tool| tool.is_destructive())
};
if is_destructive {
let response = serde_json::json!({
"content": [
{
"type": "text",
"text": format!(
"Refused: tool '{}' is classified as destructive and MCP provides no confirmation channel. \
To allow destructive tools over MCP, restart the server with SELFWARE_MCP_ALLOW_DESTRUCTIVE=1.",
tool_name
)
}
],
"isError": true
});
return (Some(response), None);
}
}
{
let needs_activate = {
let registry = self.registry.read().await;
registry.get(tool_name).is_some() && registry.get_activated(tool_name).is_none()
};
if needs_activate {
debug!(
"tools/call: activating deferred tool '{}' on demand",
tool_name
);
let mut registry = self.registry.write().await;
registry.activate(tool_name);
}
}
let result = {
let registry = self.registry.read().await;
registry.execute(tool_name, arguments).await
};
match result {
Ok(result) => {
let text = match result.as_str() {
Some(s) => s.to_string(),
None => serde_json::to_string_pretty(&result).unwrap_or_default(),
};
let response = serde_json::json!({
"content": [
{
"type": "text",
"text": text
}
],
"isError": false
});
(Some(response), None)
}
Err(err) => {
let response = serde_json::json!({
"content": [
{
"type": "text",
"text": format!("Error: {}", err)
}
],
"isError": true
});
(Some(response), None)
}
}
}
fn handle_resources_list(
&self,
_params: &Option<Value>,
) -> (Option<Value>, Option<JsonRpcError>) {
let resources = vec![
serde_json::json!({
"uri": "selfware://project/files",
"name": "Project Files",
"description": "List all project files in the working directory",
"mimeType": "application/json"
}),
serde_json::json!({
"uri": "selfware://project/structure",
"name": "Project Structure",
"description": "Directory tree of the project",
"mimeType": "text/plain"
}),
serde_json::json!({
"uri": "selfware://config",
"name": "Selfware Configuration",
"description": "Current selfware configuration",
"mimeType": "application/json"
}),
];
(Some(serde_json::json!({ "resources": resources })), None)
}
async fn handle_resources_read(
&self,
params: &Option<Value>,
) -> (Option<Value>, Option<JsonRpcError>) {
let params = match params {
Some(p) => p,
None => {
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: "Missing params for resources/read".to_string(),
data: None,
}),
);
}
};
let uri = match params.get("uri").and_then(|u| u.as_str()) {
Some(uri) => uri,
None => {
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: "Missing 'uri' parameter in resources/read".to_string(),
data: None,
}),
);
}
};
match uri {
"selfware://project/files" => {
let files = self.list_project_files();
let content = serde_json::to_string_pretty(&files).unwrap_or_default();
(
Some(serde_json::json!({
"contents": [{
"uri": uri,
"mimeType": "application/json",
"text": content
}]
})),
None,
)
}
"selfware://project/structure" => {
let tree = self.build_directory_tree(&self.project_root, 0, 4);
(
Some(serde_json::json!({
"contents": [{
"uri": uri,
"mimeType": "text/plain",
"text": tree
}]
})),
None,
)
}
"selfware://config" => {
let config = match crate::config::Config::load(self.config_path.as_deref()) {
Ok(cfg) => {
let mut value = serde_json::to_value(&cfg).unwrap_or_default();
crate::config::model::redact_config_secrets(&mut value);
serde_json::to_string_pretty(&value).unwrap_or_default()
}
Err(e) => format!("{{\"error\": \"{}\"}}", e),
};
(
Some(serde_json::json!({
"contents": [{
"uri": uri,
"mimeType": "application/json",
"text": config
}]
})),
None,
)
}
_ => {
if let Some(file_path) = uri.strip_prefix("selfware://project/file/") {
self.read_project_file(uri, file_path)
} else {
(
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: format!("Unknown resource URI: {}", uri),
data: None,
}),
)
}
}
}
}
fn list_project_files(&self) -> Vec<String> {
const MAX_FILES: usize = 10_000;
let mut files = Vec::new();
let walker = walkdir::WalkDir::new(&self.project_root)
.max_depth(8)
.into_iter()
.filter_entry(|e| {
let name = e.file_name().to_string_lossy();
!name.starts_with('.')
&& name != "target"
&& name != "node_modules"
&& name != "__pycache__"
&& name != ".git"
});
for entry in walker {
if files.len() >= MAX_FILES {
break;
}
if let Ok(entry) = entry {
if entry.file_type().is_file() {
if let Ok(relative) = entry.path().strip_prefix(&self.project_root) {
files.push(relative.to_string_lossy().to_string());
}
}
}
}
files
}
#[allow(clippy::only_used_in_recursion)]
fn build_directory_tree(&self, path: &Path, depth: usize, max_depth: usize) -> String {
if depth >= max_depth {
return String::new();
}
let mut result = String::new();
let indent = " ".repeat(depth);
let dir_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
result.push_str(&format!("{}{}/\n", indent, dir_name));
if let Ok(entries) = std::fs::read_dir(path) {
let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') || name == "target" || name == "node_modules" {
continue;
}
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
result.push_str(&self.build_directory_tree(
&entry.path(),
depth + 1,
max_depth,
));
} else {
result.push_str(&format!("{} {}\n", indent, name));
}
}
}
result
}
fn read_project_file(
&self,
uri: &str,
relative_path: &str,
) -> (Option<Value>, Option<JsonRpcError>) {
let full_path = self.project_root.join(relative_path);
match full_path.canonicalize() {
Ok(canonical) => {
if let Ok(root_canonical) = self.project_root.canonicalize() {
if !canonical.starts_with(&root_canonical) {
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: "Path escapes project root".to_string(),
data: None,
}),
);
}
}
}
Err(e) => {
return (
None,
Some(JsonRpcError {
code: INVALID_PARAMS,
message: format!("Cannot resolve path '{}': {}", relative_path, e),
data: None,
}),
);
}
}
match std::fs::read_to_string(&full_path) {
Ok(content) => {
let mime = if relative_path.ends_with(".json") {
"application/json"
} else if relative_path.ends_with(".toml") {
"application/toml"
} else if relative_path.ends_with(".yaml") || relative_path.ends_with(".yml") {
"application/yaml"
} else {
"text/plain"
};
(
Some(serde_json::json!({
"contents": [{
"uri": uri,
"mimeType": mime,
"text": content
}]
})),
None,
)
}
Err(e) => (
None,
Some(JsonRpcError {
code: INTERNAL_ERROR,
message: format!("Failed to read file '{}': {}", relative_path, e),
data: None,
}),
),
}
}
}
pub async fn run_mcp_server(
_config: &crate::config::Config,
config_path: Option<&str>,
) -> Result<()> {
eprintln!("selfware MCP server v{} starting...", SERVER_VERSION);
info!("MCP server starting on stdio transport");
let server = Arc::new(McpServer::with_config(config_path.map(|s| s.to_string())));
serve_io(server, tokio::io::stdin(), tokio::io::stdout()).await?;
eprintln!("selfware MCP server stopped.");
Ok(())
}
async fn serve_io<I, O>(server: Arc<McpServer>, input: I, output: O) -> Result<()>
where
I: tokio::io::AsyncRead + Unpin,
O: tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let mut reader = BufReader::new(input);
let (tx, mut rx) = mpsc::channel::<(String, Framing)>(256);
let request_semaphore = Arc::new(Semaphore::new(32));
let writer_task = tokio::spawn(async move {
let mut out = output;
while let Some((body, framing)) = rx.recv().await {
if let Err(e) = write_framed_message(&mut out, &body, framing).await {
eprintln!("MCP server: write error: {}", e);
}
}
});
loop {
let framing = match detect_framing(&mut reader).await {
Ok(Some(framing)) => framing,
Ok(None) => {
info!("MCP server: input closed, shutting down");
break;
}
Err(e) => {
debug!("MCP server: input read error: {}", e);
break;
}
};
let message = match framing {
Framing::ContentLength => read_content_length_message(&mut reader).await,
Framing::NewlineDelimited => read_newline_message(&mut reader).await,
};
let message = match message {
Ok(Some(msg)) => msg,
Ok(None) => {
info!("MCP server: input closed, shutting down");
break;
}
Err(e) => {
let error_response = JsonRpcResponse {
jsonrpc: "2.0",
id: Value::Null,
result: None,
error: Some(JsonRpcError {
code: PARSE_ERROR,
message: format!("Failed to read message: {}", e),
data: None,
}),
};
if let Ok(body) = serde_json::to_string(&error_response) {
let _ = tx.send((body, framing)).await;
}
continue;
}
};
let request: JsonRpcRequest = match serde_json::from_str(&message) {
Ok(req) => req,
Err(e) => {
let error_response = JsonRpcResponse {
jsonrpc: "2.0",
id: Value::Null,
result: None,
error: Some(JsonRpcError {
code: PARSE_ERROR,
message: format!("Invalid JSON: {}", e),
data: None,
}),
};
if let Ok(body) = serde_json::to_string(&error_response) {
let _ = tx.send((body, framing)).await;
}
continue;
}
};
debug!(
"MCP server received: {} (id={:?})",
request.method, request.id
);
let is_shutdown = request.method == "shutdown" || request.method == "notifications/exit";
{
let server = Arc::clone(&server);
let tx = tx.clone();
let permit = request_semaphore.clone().acquire_owned().await;
if permit.is_err() {
continue;
}
let permit = permit.unwrap();
tokio::spawn(async move {
let _permit = permit;
if let Some(resp) = server.handle_request(&request).await {
if let Ok(body) = serde_json::to_string(&resp) {
let _ = tx.send((body, framing)).await;
}
}
});
}
if is_shutdown {
info!("MCP server shutting down after shutdown request");
break;
}
}
drop(tx);
let _ = writer_task.await;
Ok(())
}
#[cfg(test)]
#[path = "../../tests/unit/mcp/server/server_test.rs"]
mod tests;