use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; use anyhow::Result; use async_trait::async_trait; use rmcp::model::{ CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult, ProtocolVersion, ServerCapabilities, }; use tokio::io::AsyncReadExt; use tokio_util::sync::CancellationToken; pub static EXTENSION_NAME: &str = "tom"; const MAX_BYTES: usize = 65_536; pub struct TomClient { info: InitializeResult, } impl TomClient { pub fn new(_context: PlatformExtensionContext) -> Result { Ok(Self { info: InitializeResult { protocol_version: ProtocolVersion::V_2025_03_26, capabilities: ServerCapabilities { tools: None, tasks: None, resources: None, prompts: None, completions: None, experimental: None, logging: None, }, server_info: Implementation { name: EXTENSION_NAME.to_string(), title: Some("Top Of Mind".to_string()), version: "1.0.0".to_string(), icons: None, website_url: None, }, instructions: None, }, }) } } #[async_trait] impl McpClientTrait for TomClient { async fn list_tools( &self, _session_id: &str, _next_cursor: Option, _cancellation_token: CancellationToken, ) -> Result { Ok(ListToolsResult { tools: vec![], next_cursor: None, meta: None, }) } async fn call_tool( &self, _session_id: &str, name: &str, _arguments: Option, _working_dir: Option<&str>, _cancellation_token: CancellationToken, ) -> Result { Ok(CallToolResult::error(vec![Content::text(format!( "tom has no tools (called: {name})" ))])) } fn get_info(&self) -> Option<&InitializeResult> { Some(&self.info) } async fn get_moim(&self, _session_id: &str) -> Option { let mut parts = Vec::new(); if let Ok(text) = std::env::var("GOOSE_MOIM_MESSAGE_TEXT") { if !text.trim().is_empty() { parts.push(truncate_utf8(text)); } } if let Ok(path) = std::env::var("GOOSE_MOIM_MESSAGE_FILE") { let expanded = shellexpand::tilde(&path); if let Some(content) = read_bounded(&expanded).await { if !content.trim().is_empty() { parts.push(content); } } } if parts.is_empty() { None } else { Some(parts.join("\n")) } } } async fn read_bounded(path: &str) -> Option { let mut file = tokio::fs::File::open(path).await.ok()?; let mut buf = vec![0u8; MAX_BYTES]; let mut total = 0; loop { let n = file.read(&mut buf[total..]).await.ok()?; if n == 0 { break; } total += n; if total >= MAX_BYTES { break; } } buf.truncate(total); let s = String::from_utf8_lossy(&buf).into_owned(); Some(truncate_utf8(s)) } fn truncate_utf8(s: String) -> String { if s.len() <= MAX_BYTES { return s; } s.char_indices() .take_while(|(i, c)| i + c.len_utf8() <= MAX_BYTES) .map(|(_, c)| c) .collect() }