use crate::types::common::{LettaId, Metadata, Timestamp};
use bon::Builder;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolType {
#[serde(rename = "letta_core")]
LettaCore,
#[serde(rename = "letta_memory_core")]
LettaMemoryCore,
#[serde(rename = "letta_multi_agent_core")]
LettaMultiAgentCore,
#[serde(rename = "letta_sleeptime_core")]
LettaSleeptimeCore,
#[serde(rename = "letta_voice_sleeptime_core")]
LettaVoiceSleeptimeCore,
#[serde(rename = "letta_builtin")]
LettaBuiltin,
#[serde(rename = "letta_files_core")]
LettaFilesCore,
#[serde(rename = "external_composio")]
ExternalComposio,
#[serde(rename = "external_langchain")]
ExternalLangchain,
#[serde(rename = "external_mcp")]
ExternalMcp,
Custom,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SourceType {
Python,
JavaScript,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipRequirement {
pub package: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_type: Option<ToolType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_type: Option<SourceType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<LettaId>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args_json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_char_limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pip_requirements: Option<Vec<PipRequirement>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_by_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated_by_id: Option<LettaId>,
#[serde(skip_serializing_if = "Option::is_none", rename = "metadata_")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<Timestamp>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<Timestamp>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)]
pub struct CreateToolRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
pub source_code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_type: Option<SourceType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args_json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_char_limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pip_requirements: Option<Vec<PipRequirement>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListToolsParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateToolRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_char_limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pip_requirements: Option<Vec<PipRequirement>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "metadata_")]
pub metadata: Option<Metadata>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum McpServerType {
Sse,
Stdio,
StreamableHttp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SseServerConfig {
pub server_name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub server_type: Option<McpServerType>,
pub server_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_header: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_headers: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct StdioServerConfig {
pub server_name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub server_type: Option<McpServerType>,
pub command: String,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct StreamableHttpServerConfig {
pub server_name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub server_type: Option<McpServerType>,
pub server_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_header: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_headers: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum McpServerConfig {
Sse(SseServerConfig),
Stdio(StdioServerConfig),
StreamableHttp(StreamableHttpServerConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub struct UpdateSseMcpServer {
#[serde(skip_serializing_if = "Option::is_none")]
pub server_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub struct UpdateStreamableHttpMcpServer {
#[serde(skip_serializing_if = "Option::is_none")]
pub server_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UpdateMcpServerRequest {
Sse(UpdateSseMcpServer),
StreamableHttp(UpdateStreamableHttpMcpServer),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct McpTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolAnnotations {
#[serde(flatten)]
pub additional_properties: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestMcpServerRequest {
#[serde(flatten)]
pub config: McpServerConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RunToolFromSourceRequest {
pub source_code: String,
pub args: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub env_vars: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_type: Option<SourceType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args_json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pip_requirements: Option<Vec<PipRequirement>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunToolFromSourceResponse {
pub id: String,
pub date: String,
pub tool_return: String,
pub status: ToolExecutionStatus,
pub tool_call_id: String,
pub name: Option<String>,
pub message_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub otid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub step_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stdout: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub stderr: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolExecutionStatus {
Success,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AppAuthSchemeAuthMode {
Oauth2,
ApiKey,
Basic,
NoAuth,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthSchemeFieldType {
Text,
Password,
Select,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthSchemeField {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "type")]
pub field_type: AuthSchemeFieldType,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppAuthScheme {
pub scheme_name: String,
pub auth_mode: AppAuthSchemeAuthMode,
pub fields: Vec<AuthSchemeField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppModel {
pub name: String,
pub key: String,
#[serde(rename = "appId")]
pub app_id: String,
pub description: String,
pub categories: Vec<String>,
pub meta: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(rename = "noAuth", skip_serializing_if = "Option::is_none")]
pub no_auth: Option<bool>,
#[serde(rename = "authSchemes", skip_serializing_if = "Option::is_none")]
pub auth_schemes: Option<Vec<AppAuthScheme>>,
#[serde(rename = "testConnectors", skip_serializing_if = "Option::is_none")]
pub test_connectors: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub triggers: Option<Vec<serde_json::Value>>,
#[serde(rename = "docs_sections", skip_serializing_if = "Option::is_none")]
pub docs_sections: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionParametersModel {
pub properties: serde_json::Map<String, serde_json::Value>,
pub title: String,
#[serde(rename = "type")]
pub schema_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionModel {
pub name: String,
pub description: String,
pub parameters: ActionParametersModel,
pub response: ActionParametersModel,
#[serde(rename = "appName")]
pub app_name: String,
#[serde(rename = "appId")]
pub app_id: String,
pub version: String,
#[serde(rename = "availableVersions")]
pub available_versions: Vec<String>,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logo: Option<String>,
#[serde(rename = "displayName", skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_tool_serialization() {
let tool = Tool {
id: Some(LettaId::from_str("tool-550e8400-e29b-41d4-a716-446655440000").unwrap()),
tool_type: Some(ToolType::Custom),
name: "calculator".to_string(),
description: Some("Basic calculator tool".to_string()),
source_type: Some(SourceType::Python),
organization_id: None,
tags: Some(vec!["math".to_string(), "utility".to_string()]),
source_code: Some("def calculate(x, y): return x + y".to_string()),
json_schema: Some(serde_json::json!({
"name": "calculator",
"parameters": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"}
}
}
})),
args_json_schema: None,
return_char_limit: Some(1000),
pip_requirements: None,
created_by_id: None,
last_updated_by_id: None,
metadata: None,
created_at: Some(chrono::Utc::now()),
updated_at: None,
};
let json = serde_json::to_string(&tool).unwrap();
let deserialized: Tool = serde_json::from_str(&json).unwrap();
assert_eq!(tool.name, deserialized.name);
}
#[test]
fn test_tool_type_serialization() {
assert_eq!(
serde_json::to_string(&ToolType::LettaCore).unwrap(),
"\"letta_core\""
);
assert_eq!(
serde_json::to_string(&ToolType::Custom).unwrap(),
"\"custom\""
);
assert_eq!(
serde_json::to_string(&ToolType::LettaFilesCore).unwrap(),
"\"letta_files_core\""
);
}
#[test]
fn test_create_tool_request() {
let request = CreateToolRequest {
description: Some("My custom tool".to_string()),
source_code: "def my_tool(): pass".to_string(),
source_type: Some(SourceType::Python),
json_schema: Some(serde_json::json!({
"name": "my_tool",
"parameters": {"type": "object"}
})),
tags: Some(vec!["custom".to_string()]),
return_char_limit: None,
pip_requirements: None,
args_json_schema: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"source_code\":\"def my_tool(): pass\""));
}
#[test]
fn test_mcp_server_configs() {
let sse_config = SseServerConfig {
server_name: "test-sse".to_string(),
server_type: Some(McpServerType::Sse),
server_url: "https://example.com/sse".to_string(),
auth_header: Some("Authorization".to_string()),
auth_token: Some("Bearer token123".to_string()),
custom_headers: None,
};
let json = serde_json::to_string(&sse_config).unwrap();
assert!(json.contains("\"server_name\":\"test-sse\""));
assert!(json.contains("\"type\":\"sse\""));
let stdio_config = StdioServerConfig {
server_name: "test-stdio".to_string(),
server_type: Some(McpServerType::Stdio),
command: "node".to_string(),
args: vec!["server.js".to_string()],
env: Some(std::collections::HashMap::from([(
"NODE_ENV".to_string(),
"production".to_string(),
)])),
};
let json = serde_json::to_string(&stdio_config).unwrap();
assert!(json.contains("\"command\":\"node\""));
let config = McpServerConfig::Sse(sse_config);
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"server_name\":\"test-sse\""));
let deserialized: McpServerConfig = serde_json::from_str(&json).unwrap();
match deserialized {
McpServerConfig::Sse(config) => assert_eq!(config.server_name, "test-sse"),
_ => panic!("Expected SSE config"),
}
}
#[test]
fn test_mcp_tool() {
let tool = McpTool {
name: "calculator".to_string(),
description: Some("Performs basic calculations".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"operation": {"type": "string", "enum": ["add", "subtract"]}
}
}),
annotations: None,
};
let json = serde_json::to_string(&tool).unwrap();
assert!(json.contains("\"name\":\"calculator\""));
assert!(json.contains("\"input_schema\""));
}
}