//! Letta MCP Server - Rust Implementation
//!
//! This is a Rust implementation of the Letta MCP Server using TurboMCP framework.
//! It maintains backward compatibility with the Node.js implementation while providing
//! better performance and type safety.
// Allow too_many_arguments for MCP tool functions - these are generated by TurboMCP
// and need all parameters for the flattened schema
#![allow(clippy::too_many_arguments)]
// Allow unexpected_cfgs from turbomcp macro expansion
#![allow(unexpected_cfgs)]
use serde_json::Value;
use std::sync::Arc;
use turbomcp::prelude::*;
pub mod tools;
// Use official Letta SDK
use letta::{auth::AuthConfig, ClientConfig, LettaClient};
use tools::{
agent_advanced, file_folder_ops, job_monitor, mcp_ops, memory_unified, source_manager,
tool_manager,
};
/// Main Letta MCP Server
#[derive(Clone)]
pub struct LettaServer {
client: Arc<LettaClient>,
}
#[turbomcp::server(
name = "letta-mcp-server",
version = "2.0.1",
description = "MCP server for Letta AI - comprehensive API coverage with 7 consolidated tools (97 operations)"
)]
impl LettaServer {
/// Create a new Letta MCP Server instance
pub fn new(base_url: String, password: String) -> Result<Self, Box<dyn std::error::Error>> {
tracing::info!("Initializing Letta MCP Server");
// Normalize base URL: The Rust SDK hardcodes "v1/" prefixes in API paths,
// so the base URL should NOT include "/v1". Strip it if present.
// Also remove any trailing slashes for consistency.
let normalized_url = base_url
.trim_end_matches('/')
.trim_end_matches("/v1")
.trim_end_matches('/')
.to_string();
tracing::info!("Base URL (normalized): {}", normalized_url);
tracing::info!("Password length: {}", password.len());
// Configure the official Letta SDK client
let config = ClientConfig::new(&normalized_url)?.auth(AuthConfig::bearer(&password));
let client = LettaClient::new(config)?;
tracing::info!("Letta SDK client initialized successfully");
Ok(Self {
client: Arc::new(client),
})
}
// ========================================
// CONSOLIDATED TOOL 1: Agent Advanced
// ========================================
#[tool(
description = "Advanced agent operations hub - Supports 22 operations including CRUD (list, create, get, update, delete), tools (list_tools), messaging (send_message), management (export, import, clone, get_config, bulk_delete), advanced (context, reset_messages, summarize, stream), async (async_message, cancel_message), and utility (preview_payload, search_messages, get_message, count) operations."
)]
async fn letta_agent_advanced(
&self,
request: agent_advanced::AgentAdvancedRequest,
) -> McpResult<String> {
// Call handler directly - TurboMCP will auto-detect flattening and use schemars schema
agent_advanced::handle_agent_advanced(&self.client, request).await
}
// ========================================
// CONSOLIDATED TOOL 2: Memory Unified
// ========================================
#[tool(
description = "Unified Memory Operations Hub - Provides unified interface for all memory operations. Supports 15 operations including core memory (get_core_memory, update_core_memory), blocks (get_block_by_label, list_blocks, create_block, get_block, update_block, attach_block, detach_block, list_agents_using_block), and archival memory (search_archival, list_passages, create_passage, update_passage, delete_passage)."
)]
async fn letta_memory_unified(
&self,
operation: String,
agent_id: Option<String>,
block_id: Option<String>,
block_label: Option<String>,
passage_id: Option<String>,
label: Option<String>,
value: Option<String>,
text: Option<String>,
query: Option<String>,
limit: Option<i32>,
offset: Option<i32>,
is_template: Option<bool>,
) -> McpResult<String> {
// Parse operation from string
let op = serde_json::from_value(serde_json::Value::String(operation))
.map_err(|e| McpError::invalid_request(format!("Invalid operation: {}", e)))?;
// Create request from individual parameters
let request = memory_unified::MemoryUnifiedRequest {
operation: op,
agent_id,
block_id,
block_label,
passage_id,
label,
value,
text,
query,
limit,
offset,
is_template,
request_heartbeat: None,
};
// Call handler
let response = memory_unified::handle_memory_unified(&self.client, request).await?;
// Serialize to JSON string for MCP response
serde_json::to_string(&response)
.map_err(|e| McpError::internal(format!("Failed to serialize response: {}", e)))
}
// ========================================
// CONSOLIDATED TOOL 3: Tool Manager
// ========================================
#[tool(
description = "Tool Manager Operations Hub - Provides unified interface for tool management operations. Supports 13 operations including CRUD (list, get, create, update, delete, upsert), agent operations (attach, detach, bulk_attach), and advanced operations (generate_from_prompt, generate_schema, run_from_source, add_base_tools)."
)]
async fn letta_tool_manager(
&self,
operation: String,
tool_id: Option<String>,
agent_id: Option<String>,
agent_ids: Option<Vec<String>>,
source_code: Option<String>,
source_type: Option<String>,
tags: Option<Vec<String>>,
description: Option<String>,
json_schema: Option<Value>,
args_json_schema: Option<Value>,
return_char_limit: Option<u32>,
args: Option<Value>,
env_vars: Option<std::collections::HashMap<String, String>>,
name: Option<String>,
) -> McpResult<String> {
// Parse operation from string
let op = serde_json::from_value(serde_json::Value::String(operation))
.map_err(|e| McpError::invalid_request(format!("Invalid operation: {}", e)))?;
// Create request from individual parameters
let request = tool_manager::ToolManagerRequest {
operation: op,
tool_id,
agent_id,
agent_ids,
source_code,
source_type,
tags,
description,
json_schema,
args_json_schema,
return_char_limit,
args,
env_vars,
name,
request_heartbeat: None,
limit: None,
offset: None,
};
// Call handler
let response = tool_manager::handle_tool_manager(&self.client, request).await?;
// Serialize to JSON string for MCP response
serde_json::to_string(&response)
.map_err(|e| McpError::internal(format!("Failed to serialize response: {}", e)))
}
// ========================================
// CONSOLIDATED TOOL 4: Source Manager
// ========================================
#[tool(
description = "Source Manager Operations Hub - Provides unified interface for source management operations. Supports 15 operations including CRUD (list, get, create, update, delete, count), agent operations (attach, detach, list_attached), file operations (upload, delete_files, list_files), and folder operations (list_folders, get_folder_contents, list_agents_using)."
)]
async fn letta_source_manager(
&self,
operation: String,
source_id: Option<String>,
agent_id: Option<String>,
name: Option<String>,
description: Option<String>,
file_id: Option<String>,
file_name: Option<String>,
file_data: Option<String>,
content_type: Option<String>,
limit: Option<i32>,
include_content: Option<bool>,
) -> McpResult<String> {
// Parse operation from string
let op = serde_json::from_value(serde_json::Value::String(operation))
.map_err(|e| McpError::invalid_request(format!("Invalid operation: {}", e)))?;
// Create request from individual parameters
let request = source_manager::SourceManagerRequest {
operation: op,
source_id,
agent_id,
name,
description,
file_id,
file_name,
file_data,
content_type,
limit,
include_content,
request_heartbeat: None,
};
// Call handler
let response = source_manager::handle_source_manager(&self.client, request).await?;
// Serialize to JSON string for MCP response
serde_json::to_string(&response)
.map_err(|e| McpError::internal(format!("Failed to serialize response: {}", e)))
}
// ========================================
// CONSOLIDATED TOOL 5: Job Monitor
// ========================================
#[tool(
description = "Job Monitor Operations Hub - Provides unified interface for job monitoring operations. Supports 4 operations: list (all jobs), get (specific job), cancel (job cancellation), and list_active (active jobs only)."
)]
async fn letta_job_monitor(
&self,
operation: String,
job_id: Option<String>,
limit: Option<i32>,
) -> McpResult<String> {
// Parse operation
let op = match operation.as_str() {
"list" => job_monitor::JobOperation::List,
"get" => job_monitor::JobOperation::Get,
"cancel" => job_monitor::JobOperation::Cancel,
"list_active" => job_monitor::JobOperation::ListActive,
_ => {
return Err(McpError::invalid_request(format!(
"Unknown operation: {}",
operation
)))
}
};
// Create request from individual parameters
let request = job_monitor::JobMonitorRequest {
operation: op,
job_id,
limit,
request_heartbeat: None,
};
// Call handler
let response = job_monitor::handle_job_monitor(&self.client, request).await?;
// Serialize to JSON string for MCP response
serde_json::to_string(&response)
.map_err(|e| McpError::internal(format!("Failed to serialize response: {}", e)))
}
// ========================================
// CONSOLIDATED TOOL 6: File/Folder Ops
// ========================================
#[tool(
description = "File and Folder Management Hub - Provides unified interface for file session management and folder operations. Supports 8 operations including file sessions (list_files, open_file, close_file, close_all_files) and folder operations (list_folders, attach_folder, detach_folder, list_agents_in_folder)."
)]
async fn letta_file_folder_ops(
&self,
operation: String,
agent_id: Option<String>,
file_id: Option<String>,
folder_id: Option<String>,
limit: Option<usize>,
offset: Option<usize>,
) -> McpResult<String> {
// Create request from individual parameters
let request = file_folder_ops::FileFolderRequest {
operation,
agent_id,
file_id,
folder_id,
limit,
offset,
request_heartbeat: None,
};
// Call handler
let response = file_folder_ops::handle_file_folder_ops(&self.client, request).await?;
// Serialize to JSON string for MCP response
serde_json::to_string(&response)
.map_err(|e| McpError::internal(format!("Failed to serialize response: {}", e)))
}
// ========================================
// CONSOLIDATED TOOL 7: MCP Operations
// ========================================
#[tool(
description = "MCP Server Operations Hub - Unified tool for complete MCP server lifecycle management. Supports 10 operations: add, update, delete, test, connect, resync (server management) and list_servers, list_tools, register_tool, execute (tool operations)."
)]
async fn letta_mcp_ops(
&self,
operation: String,
server_name: Option<String>,
server_config: Option<Value>,
tool_name: Option<String>,
tool_args: Option<Value>,
oauth_config: Option<Value>,
pagination: Option<Value>,
) -> McpResult<String> {
// Parse operation from string
let op = serde_json::from_value(serde_json::Value::String(operation))
.map_err(|e| McpError::invalid_request(format!("Invalid operation: {}", e)))?;
// Create request from individual parameters
let request = mcp_ops::McpOpsRequest {
operation: op,
server_name,
server_config,
tool_name,
tool_args,
oauth_config,
pagination,
request_heartbeat: None,
};
// Call handler
let response = mcp_ops::handle_mcp_ops(&self.client, request).await?;
// Serialize to JSON string for MCP response
serde_json::to_string(&response)
.map_err(|e| McpError::internal(format!("Failed to serialize response: {}", e)))
}
}
// Custom HTTP runner implementation with permissive security for development
#[cfg(feature = "http")]
impl LettaServer {
/// Run HTTP server with custom security configuration
pub async fn run_http_custom(&self, addr: &str) -> Result<(), Box<dyn std::error::Error>> {
use std::sync::Arc;
use std::time::Duration;
use turbomcp_transport::streamable_http_v2::{run_server, StreamableHttpConfigBuilder};
// Create permissive HTTP config for development
let config = StreamableHttpConfigBuilder::new()
.with_bind_address(addr)
.allow_any_origin(true) // Allow any origin in development mode
.allow_localhost(true)
.with_rate_limit(1_000_000, Duration::from_secs(60)) // Very high limit for development
.build();
// Run the HTTP server with custom config
run_server(config, Arc::new(self.clone())).await?;
Ok(())
}
}