use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
pub const SMCP_NAMESPACE: &str = "/smcp";
pub mod error_codes {
pub const BAD_REQUEST: i32 = 400;
pub const UNAUTHORIZED: i32 = 401;
pub const FORBIDDEN: i32 = 403;
pub const NOT_FOUND: i32 = 404;
pub const TIMEOUT: i32 = 408;
pub const INTERNAL_ERROR: i32 = 500;
pub const TOOL_NOT_FOUND: i32 = 4001;
pub const TOOL_DISABLED: i32 = 4002;
pub const TOOL_EXECUTION_FAILED: i32 = 4003;
pub const TOOL_TIMEOUT: i32 = 4004;
pub const TOOL_REQUIRES_CONFIRMATION: i32 = 4005;
pub const ROOM_FULL: i32 = 4101;
pub const ROOM_NOT_FOUND: i32 = 4102;
pub const NOT_IN_ROOM: i32 = 4103;
pub const CROSS_ROOM_ACCESS: i32 = 4104;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetail {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<HashMap<String, serde_json::Value>>,
}
impl ErrorDetail {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
}
}
pub fn with_detail(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
let details = self.details.get_or_insert_with(HashMap::new);
details.insert(key.into(), value.into());
self
}
pub fn with_details(mut self, details: HashMap<String, serde_json::Value>) -> Self {
self.details = Some(details);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: ErrorDetail,
}
impl ErrorResponse {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
error: ErrorDetail::new(code, message),
}
}
pub fn with_detail(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.error = self.error.with_detail(key, value);
self
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(error_codes::BAD_REQUEST, message)
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::new(error_codes::UNAUTHORIZED, message)
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::new(error_codes::FORBIDDEN, message)
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(error_codes::NOT_FOUND, message)
}
pub fn timeout(message: impl Into<String>) -> Self {
Self::new(error_codes::TIMEOUT, message)
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(error_codes::INTERNAL_ERROR, message)
}
pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
let name = tool_name.into();
Self::new(
error_codes::TOOL_NOT_FOUND,
format!("Tool '{}' not found", name),
)
.with_detail("tool_name", serde_json::Value::String(name))
}
pub fn tool_execution_failed(message: impl Into<String>) -> Self {
Self::new(error_codes::TOOL_EXECUTION_FAILED, message)
}
pub fn tool_timeout(timeout_secs: u64) -> Self {
Self::new(
error_codes::TOOL_TIMEOUT,
format!("Tool execution timed out after {} seconds", timeout_secs),
)
.with_detail(
"timeout",
serde_json::Value::Number(serde_json::Number::from(timeout_secs)),
)
}
pub fn room_full(office_id: impl Into<String>) -> Self {
let id = office_id.into();
Self::new(
error_codes::ROOM_FULL,
format!("Room '{}' already has an agent", id),
)
.with_detail("office_id", serde_json::Value::String(id))
}
pub fn not_in_room() -> Self {
Self::new(error_codes::NOT_IN_ROOM, "Session is not in any room")
}
}
pub mod events {
pub const CLIENT_GET_TOOLS: &str = "client:get_tools";
pub const CLIENT_GET_CONFIG: &str = "client:get_config";
pub const CLIENT_GET_DESKTOP: &str = "client:get_desktop";
pub const CLIENT_TOOL_CALL: &str = "client:tool_call";
pub const SERVER_JOIN_OFFICE: &str = "server:join_office";
pub const SERVER_LEAVE_OFFICE: &str = "server:leave_office";
pub const SERVER_UPDATE_CONFIG: &str = "server:update_config";
pub const SERVER_UPDATE_TOOL_LIST: &str = "server:update_tool_list";
pub const SERVER_UPDATE_DESKTOP: &str = "server:update_desktop";
pub const SERVER_TOOL_CALL_CANCEL: &str = "server:tool_call_cancel";
pub const SERVER_LIST_ROOM: &str = "server:list_room";
pub const NOTIFY_TOOL_CALL_CANCEL: &str = "notify:tool_call_cancel";
pub const NOTIFY_ENTER_OFFICE: &str = "notify:enter_office";
pub const NOTIFY_LEAVE_OFFICE: &str = "notify:leave_office";
pub const NOTIFY_UPDATE_CONFIG: &str = "notify:update_config";
pub const NOTIFY_UPDATE_TOOL_LIST: &str = "notify:update_tool_list";
pub const NOTIFY_UPDATE_DESKTOP: &str = "notify:update_desktop";
pub const NOTIFY_PREFIX: &str = "notify:";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ReqId(pub String);
impl ReqId {
pub fn new() -> Self {
Self(Uuid::new_v4().simple().to_string())
}
pub fn from_string(s: String) -> Self {
Self(s)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ReqId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Agent,
Computer,
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::Agent => write!(f, "agent"),
Role::Computer => write!(f, "computer"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
pub name: String,
pub role: Role,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallReq {
#[serde(flatten)]
pub base: AgentCallData,
pub computer: String,
pub tool_name: String,
pub params: serde_json::Value,
pub timeout: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetComputerConfigReq {
#[serde(flatten)]
pub base: AgentCallData,
pub computer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateComputerConfigReq {
pub computer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetComputerConfigRet {
#[serde(skip_serializing_if = "Option::is_none")]
pub inputs: Option<Vec<serde_json::Value>>,
pub servers: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRet {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<serde_json::Value>>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub req_id: Option<ReqId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetToolsReq {
#[serde(flatten)]
pub base: AgentCallData,
pub computer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SMCPTool {
pub name: String,
pub description: String,
pub params_schema: serde_json::Value,
pub return_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetToolsRet {
pub tools: Vec<SMCPTool>,
pub req_id: ReqId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCallData {
pub agent: String,
pub req_id: ReqId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnterOfficeReq {
pub role: Role,
pub name: String,
pub office_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaveOfficeReq {
pub office_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetDesktopReq {
#[serde(flatten)]
pub base: AgentCallData,
pub computer: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub desktop_size: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window: Option<String>,
}
pub type Desktop = String;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetDesktopRet {
#[serde(skip_serializing_if = "Option::is_none")]
pub desktops: Option<Vec<Desktop>>,
pub req_id: ReqId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListRoomReq {
#[serde(flatten)]
pub base: AgentCallData,
pub office_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub sid: String,
pub name: String,
pub role: Role,
pub office_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListRoomRet {
pub sessions: Vec<SessionInfo>,
pub req_id: ReqId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnterOfficeNotification {
pub office_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub computer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaveOfficeNotification {
pub office_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub computer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateMCPConfigNotification {
pub computer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateToolListNotification {
pub computer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Notification {
ToolCallCancel,
EnterOffice(EnterOfficeNotification),
LeaveOffice(LeaveOfficeNotification),
UpdateMCPConfig(UpdateMCPConfigNotification),
UpdateToolList(UpdateToolListNotification),
UpdateDesktop,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_req_id_helpers() {
let req_id = ReqId::new();
assert!(!req_id.as_str().is_empty());
let req_id2 = ReqId::from_string("abc".to_string());
assert_eq!(req_id2.as_str(), "abc");
let req_id3 = ReqId::default();
assert!(!req_id3.as_str().is_empty());
}
#[test]
fn test_role_serde_lowercase() {
let json = serde_json::to_string(&Role::Agent).unwrap();
assert_eq!(json, "\"agent\"");
let de: Role = serde_json::from_str("\"computer\"").unwrap();
assert!(matches!(de, Role::Computer));
}
#[test]
fn test_notification_serde() {
let n = Notification::EnterOffice(EnterOfficeNotification {
office_id: "office1".to_string(),
computer: Some("c1".to_string()),
agent: None,
});
let json = serde_json::to_string(&n).unwrap();
let de: Notification = serde_json::from_str(&json).unwrap();
match de {
Notification::EnterOffice(p) => {
assert_eq!(p.office_id, "office1");
assert_eq!(p.computer.as_deref(), Some("c1"));
assert!(p.agent.is_none());
}
_ => panic!("unexpected notification"),
}
}
#[test]
fn test_tool_call_ret_mcp_format() {
let success_ret = ToolCallRet {
content: Some(vec![serde_json::json!({
"type": "text",
"text": "Operation completed successfully"
})]),
is_error: Some(false),
req_id: Some(ReqId::from_string("test123".to_string())),
};
let json = serde_json::to_string(&success_ret).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("content").is_some());
assert!(parsed.get("isError").is_some());
assert_eq!(parsed.get("isError").unwrap(), false);
assert_eq!(parsed.get("req_id").unwrap().as_str().unwrap(), "test123");
assert!(json.contains("isError"));
assert!(!json.contains("is_error"));
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("success").is_none());
assert!(parsed.get("result").is_none());
assert!(parsed.get("error").is_none());
}
#[test]
fn test_tool_call_ret_error_format() {
let error_ret = ToolCallRet {
content: Some(vec![serde_json::json!({
"type": "text",
"text": "Tool execution failed"
})]),
is_error: Some(true),
req_id: None,
};
let json = serde_json::to_string(&error_ret).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("content").is_some());
assert_eq!(parsed.get("isError").unwrap(), true);
assert!(parsed.get("req_id").is_none());
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("success").is_none());
assert!(parsed.get("result").is_none());
assert!(parsed.get("error").is_none());
}
#[test]
fn test_tool_call_ret_minimal() {
let minimal_ret = ToolCallRet {
content: None,
is_error: None,
req_id: None,
};
let json = serde_json::to_string(&minimal_ret).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, serde_json::json!({}));
}
#[test]
fn test_tool_call_ret_roundtrip() {
let original = ToolCallRet {
content: Some(vec![serde_json::json!({
"type": "text",
"text": "Test result"
})]),
is_error: Some(false),
req_id: Some(ReqId::new()),
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: ToolCallRet = serde_json::from_str(&json).unwrap();
assert_eq!(original.content, deserialized.content);
assert_eq!(original.is_error, deserialized.is_error);
assert_eq!(original.req_id, deserialized.req_id);
}
#[test]
fn test_error_response_format() {
let error_resp = ErrorResponse::new(404, "Resource not found");
let json = serde_json::to_string(&error_resp).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.get("error").is_some());
let error = parsed.get("error").unwrap();
assert_eq!(error.get("code").unwrap(), 404);
assert_eq!(error.get("message").unwrap(), "Resource not found");
assert!(error.get("details").is_none()); }
#[test]
fn test_error_response_with_details() {
let error_resp = ErrorResponse::tool_not_found("my_tool");
let json = serde_json::to_string(&error_resp).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let error = parsed.get("error").unwrap();
assert_eq!(error.get("code").unwrap(), error_codes::TOOL_NOT_FOUND);
assert!(error
.get("message")
.unwrap()
.as_str()
.unwrap()
.contains("my_tool"));
assert!(error.get("details").is_some());
assert_eq!(
error.get("details").unwrap().get("tool_name").unwrap(),
"my_tool"
);
}
#[test]
fn test_error_response_convenience_constructors() {
assert_eq!(ErrorResponse::bad_request("test").error.code, 400);
assert_eq!(ErrorResponse::unauthorized("test").error.code, 401);
assert_eq!(ErrorResponse::forbidden("test").error.code, 403);
assert_eq!(ErrorResponse::not_found("test").error.code, 404);
assert_eq!(ErrorResponse::timeout("test").error.code, 408);
assert_eq!(ErrorResponse::internal_error("test").error.code, 500);
assert_eq!(ErrorResponse::tool_not_found("t").error.code, 4001);
assert_eq!(ErrorResponse::tool_execution_failed("t").error.code, 4003);
assert_eq!(ErrorResponse::tool_timeout(30).error.code, 4004);
assert_eq!(ErrorResponse::room_full("office1").error.code, 4101);
assert_eq!(ErrorResponse::not_in_room().error.code, 4103);
}
#[test]
fn test_error_response_roundtrip() {
let original = ErrorResponse::new(500, "Internal error")
.with_detail("trace_id", serde_json::Value::String("abc123".to_string()));
let json = serde_json::to_string(&original).unwrap();
let deserialized: ErrorResponse = serde_json::from_str(&json).unwrap();
assert_eq!(original.error.code, deserialized.error.code);
assert_eq!(original.error.message, deserialized.error.message);
assert_eq!(
original.error.details.as_ref().unwrap().get("trace_id"),
deserialized.error.details.as_ref().unwrap().get("trace_id")
);
}
}