1use post_cortex_memory::ConversationMemorySystem;
22use crate::daemon::coerce::CoercionError;
23use post_cortex_mcp::MCPToolResult;
24use rmcp::{
25 RoleServer, ServerHandler,
26 handler::server::router::tool::ToolRouter,
27 handler::server::wrapper::Parameters,
28 model::{
29 CallToolRequestParam, CallToolResult, Content, ErrorData as McpError, ListToolsResult,
30 PaginatedRequestParam, *,
31 },
32 service::RequestContext,
33 tool, tool_router,
34};
35use std::sync::Arc;
36use tracing::info;
37
38mod admin;
39mod assembly;
40mod entity;
41mod query;
42mod requests;
43mod search;
44mod session;
45mod summary;
46mod update_context;
47mod workspace;
48
49pub use requests::{
52 AdminRequest, AssembleContextRequest, ContextUpdateItem, GetStructuredSummaryRequest,
53 ManageEntityRequest, ManageWorkspaceRequest, QueryConversationContextRequest,
54 SemanticSearchRequest, SessionRequest, UpdateConversationContextRequest,
55};
56
57#[derive(Clone)]
59pub struct PostCortexService {
60 memory_system: Arc<ConversationMemorySystem>,
61 tool_router: ToolRouter<PostCortexService>,
62}
63
64pub(crate) fn mcp_result_to_call_result(result: MCPToolResult) -> CallToolResult {
74 let mut contents = vec![Content::text(result.message)];
75 if let Some(data) = result.data {
76 contents.push(Content::text(format!(
77 "\n\nResults:\n{}",
78 serde_json::to_string_pretty(&data).unwrap_or_default()
79 )));
80 }
81 CallToolResult::success(contents)
82}
83
84pub(crate) async fn check_session_exists_for_dry_run(
87 memory_system: &Arc<ConversationMemorySystem>,
88 session_id: uuid::Uuid,
89) -> Result<(), McpError> {
90 match memory_system.storage_actor.load_session(session_id).await {
91 Ok(Some(_)) => Ok(()),
92 Ok(None) => Err(CoercionError::new(
93 "Session not found",
94 std::io::Error::new(std::io::ErrorKind::NotFound, "session does not exist"),
95 Some(serde_json::Value::String(session_id.to_string())),
96 )
97 .with_parameter_path("session_id".to_string())
98 .with_hint("Create a session first using the 'session' tool with action='create'")
99 .to_mcp_error()),
100 Err(e) => Err(McpError::internal_error(
101 format!("Failed to check session existence: {}", e),
102 None,
103 )),
104 }
105}
106
107pub(crate) fn parse_date_range(
110 date_from: Option<String>,
111 date_to: Option<String>,
112) -> Result<Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>, McpError> {
113 match (date_from, date_to) {
114 (Some(from_str), Some(to_str)) => {
115 let from = chrono::DateTime::parse_from_rfc3339(&from_str)
116 .map_err(|_| {
117 McpError::invalid_params(
118 format!(
119 "Invalid date_from format: '{}'. Expected RFC3339 format (e.g., 2024-01-01T00:00:00Z)",
120 from_str
121 ),
122 Some(serde_json::Value::String("date_from".to_string())),
123 )
124 })?
125 .with_timezone(&chrono::Utc);
126 let to = chrono::DateTime::parse_from_rfc3339(&to_str)
127 .map_err(|_| {
128 McpError::invalid_params(
129 format!(
130 "Invalid date_to format: '{}'. Expected RFC3339 format (e.g., 2024-12-31T23:59:59Z)",
131 to_str
132 ),
133 Some(serde_json::Value::String("date_to".to_string())),
134 )
135 })?
136 .with_timezone(&chrono::Utc);
137 Ok(Some((from, to)))
138 }
139 (Some(_), None) | (None, Some(_)) => Err(McpError::invalid_params(
140 "Both date_from and date_to must be provided together".to_string(),
141 Some(serde_json::Value::String("date_from,date_to".to_string())),
142 )),
143 (None, None) => Ok(None),
144 }
145}
146
147#[tool_router]
152impl PostCortexService {
153 pub fn new(memory_system: Arc<ConversationMemorySystem>) -> Self {
155 info!("Initializing Post-Cortex MCP Service (9 consolidated tools)");
156 Self {
157 memory_system,
158 tool_router: Self::tool_router(),
159 }
160 }
161
162 #[tool(
163 description = "Manage sessions. Actions: create, list, load, search, update_metadata, delete.",
164 input_schema = rmcp::handler::server::common::schema_for_type::<SessionRequest>()
165 )]
166 async fn session(
167 &self,
168 params: Parameters<serde_json::Value>,
169 ) -> Result<CallToolResult, McpError> {
170 session::handle(&self.memory_system, params).await
171 }
172
173 #[tool(
174 description = "Add context updates to conversation. Supports single update or bulk mode with updates array.",
175 input_schema = rmcp::handler::server::common::schema_for_type::<UpdateConversationContextRequest>()
176 )]
177 async fn update_conversation_context(
178 &self,
179 params: Parameters<serde_json::Value>,
180 ) -> Result<CallToolResult, McpError> {
181 update_context::handle(&self.memory_system, params).await
182 }
183
184 #[tool(
185 description = "Universal semantic search. Scope: session (requires scope_id), workspace (requires scope_id), or global (default).",
186 input_schema = rmcp::handler::server::common::schema_for_type::<SemanticSearchRequest>()
187 )]
188 async fn semantic_search(
189 &self,
190 params: Parameters<serde_json::Value>,
191 ) -> Result<CallToolResult, McpError> {
192 search::handle(&self.memory_system, params).await
193 }
194
195 #[tool(
196 description = "Get session summary. Use 'include' to specify sections: decisions, insights, entities, or all (default).",
197 input_schema = rmcp::handler::server::common::schema_for_type::<GetStructuredSummaryRequest>()
198 )]
199 async fn get_structured_summary(
200 &self,
201 params: Parameters<serde_json::Value>,
202 ) -> Result<CallToolResult, McpError> {
203 summary::handle(&self.memory_system, params).await
204 }
205
206 #[tool(
207 description = "Query session data. Types include: find_related_entities, get_entity_context, search_updates, entity_importance, entity_network, find_related_content, key_decisions, key_insights, session_statistics, structured_summary, decisions, open_questions, assemble_context.",
208 input_schema = rmcp::handler::server::common::schema_for_type::<QueryConversationContextRequest>()
209 )]
210 async fn query_conversation_context(
211 &self,
212 params: Parameters<serde_json::Value>,
213 ) -> Result<CallToolResult, McpError> {
214 query::handle(&self.memory_system, params).await
215 }
216
217 #[tool(
218 description = "Manage workspaces. Actions: create, list, get, delete, add_session, remove_session",
219 input_schema = rmcp::handler::server::common::schema_for_type::<ManageWorkspaceRequest>()
220 )]
221 async fn manage_workspace(
222 &self,
223 params: Parameters<serde_json::Value>,
224 ) -> Result<CallToolResult, McpError> {
225 workspace::handle(&self.memory_system, params).await
226 }
227
228 #[tool(
229 description = "Graph-aware context assembly: blends semantic search with entity-graph traversal and impact analysis. Returns ranked items, entity neighbourhood, dependency impact, and an LLM-ready formatted block. Scope: session or workspace.",
230 input_schema = rmcp::handler::server::common::schema_for_type::<AssembleContextRequest>()
231 )]
232 async fn assemble_context(
233 &self,
234 params: Parameters<serde_json::Value>,
235 ) -> Result<CallToolResult, McpError> {
236 assembly::handle(&self.memory_system, params).await
237 }
238
239 #[tool(
240 description = "Manage entities and context entries in a session. Actions: delete (remove entity + cascade typed edges; requires entity_name), delete_update (remove a single context entry by entry_id).",
241 input_schema = rmcp::handler::server::common::schema_for_type::<ManageEntityRequest>()
242 )]
243 async fn manage_entity(
244 &self,
245 params: Parameters<serde_json::Value>,
246 ) -> Result<CallToolResult, McpError> {
247 entity::handle(&self.memory_system, params).await
248 }
249
250 #[tool(
251 description = "Daemon administration. Actions: health, vectorize_session (session_id), vectorize_stats, create_checkpoint (session_id).",
252 input_schema = rmcp::handler::server::common::schema_for_type::<AdminRequest>()
253 )]
254 async fn admin(
255 &self,
256 params: Parameters<serde_json::Value>,
257 ) -> Result<CallToolResult, McpError> {
258 admin::handle(&self.memory_system, params).await
259 }
260}
261
262impl ServerHandler for PostCortexService {
266 fn get_info(&self) -> ServerInfo {
267 ServerInfo {
268 protocol_version: ProtocolVersion::V_2024_11_05,
269 capabilities: ServerCapabilities::builder().enable_tools().build(),
270 server_info: Implementation::from_build_env(),
271 instructions: Some(
272 "Post-Cortex: Intelligent conversation memory system with 9 consolidated MCP tools. \
273 Tools: session, update_conversation_context, semantic_search, get_structured_summary, \
274 query_conversation_context, manage_workspace, assemble_context, manage_entity, admin. \
275 All use shared RocksDB for centralized knowledge management.".to_string()
276 ),
277 }
278 }
279
280 async fn initialize(
281 &self,
282 _request: InitializeRequestParam,
283 context: RequestContext<RoleServer>,
284 ) -> Result<InitializeResult, McpError> {
285 if let Some(parts) = context.extensions.get::<axum::http::request::Parts>() {
286 info!("Client initialized from {}", parts.uri);
287 }
288 Ok(self.get_info())
289 }
290
291 async fn list_tools(
293 &self,
294 _request: Option<PaginatedRequestParam>,
295 _context: RequestContext<RoleServer>,
296 ) -> Result<ListToolsResult, McpError> {
297 let tools = self.tool_router.list_all();
298
299 let tools: Vec<Tool> = tools
302 .into_iter()
303 .map(|mut tool| {
304 let mut schema = (*tool.input_schema).clone();
305 schema.remove("$schema");
306 if let Some(defs) = schema.get_mut("$defs") {
308 if let Some(defs_obj) = defs.as_object_mut() {
309 for (_, def) in defs_obj.iter_mut() {
310 if let Some(def_obj) = def.as_object_mut() {
311 def_obj.remove("$schema");
312 }
313 }
314 }
315 }
316 tool.input_schema = std::sync::Arc::new(schema);
317 tool
318 })
319 .collect();
320
321 Ok(ListToolsResult {
322 tools,
323 meta: None,
324 next_cursor: None,
325 })
326 }
327
328 async fn call_tool(
330 &self,
331 request: CallToolRequestParam,
332 context: RequestContext<RoleServer>,
333 ) -> Result<CallToolResult, McpError> {
334 let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
335 self.tool_router.call(tcc).await
336 }
337}