Skip to main content

do_memory_mcp/server/tools/
episode_relationships.rs

1//! Episode relationship tool handlers for MCP server
2//!
3//! This module provides handlers for episode relationship tools:
4//! - add_episode_relationship: Add a relationship between two episodes
5//! - remove_episode_relationship: Remove a relationship by ID
6//! - get_episode_relationships: Get relationships for an episode
7//! - find_related_episodes: Find episodes related to a given episode
8//! - check_relationship_exists: Check if a specific relationship exists
9//! - get_dependency_graph: Get relationship graph for visualization
10//! - validate_no_cycles: Check if adding a relationship would create a cycle
11//! - get_topological_order: Get topological ordering of episodes
12
13use crate::mcp::tools::episode_relationships::{
14    AddEpisodeRelationshipInput, CheckRelationshipExistsInput, DependencyGraphInput,
15    EpisodeRelationshipTools, FindRelatedEpisodesInput, GetEpisodeRelationshipsInput,
16    GetTopologicalOrderInput, RemoveEpisodeRelationshipInput, ValidateNoCyclesInput,
17};
18use crate::server::MemoryMCPServer;
19use anyhow::Result;
20use serde_json::Value;
21use tracing::{debug, info};
22
23impl MemoryMCPServer {
24    /// Add a relationship between two episodes
25    ///
26    /// This tool creates a directed relationship from one episode to another
27    /// with validation. Supports relationship types: parent_child, depends_on,
28    /// follows, related_to, blocks, duplicates, references.
29    ///
30    /// # Arguments (from JSON)
31    ///
32    /// * `from_episode_id` - Source episode UUID
33    /// * `to_episode_id` - Target episode UUID
34    /// * `relationship_type` - Type of relationship
35    /// * `reason` - Optional explanation
36    /// * `priority` - Optional priority (1-10)
37    /// * `created_by` - Optional creator identifier
38    pub async fn add_episode_relationship_tool(&self, args: Value) -> Result<Value> {
39        debug!("Adding episode relationship with args: {}", args);
40
41        let input: AddEpisodeRelationshipInput = serde_json::from_value(args)?;
42        let from_id = input.from_episode_id.clone();
43        let to_id = input.to_episode_id.clone();
44        let rel_type = input.relationship_type.clone();
45        let client_id = input
46            .created_by
47            .clone()
48            .unwrap_or_else(|| "unknown".to_string());
49
50        let tools = EpisodeRelationshipTools::new(self.memory());
51        let result = tools.add_relationship(input).await;
52
53        // Log the operation to audit trail
54        let audit_logger = self.audit_logger();
55        match &result {
56            Ok(r) => {
57                audit_logger
58                    .log_add_relationship(
59                        &client_id,
60                        &from_id,
61                        &to_id,
62                        &rel_type,
63                        &r.relationship_id,
64                        true,
65                    )
66                    .await;
67
68                info!(
69                    relationship_id = %r.relationship_id,
70                    from_episode_id = %from_id,
71                    to_episode_id = %to_id,
72                    relationship_type = %rel_type,
73                    "Created episode relationship via MCP"
74                );
75            }
76            Err(e) => {
77                audit_logger
78                    .log_add_relationship(&client_id, &from_id, &to_id, &rel_type, "none", false)
79                    .await;
80
81                debug!("Failed to create episode relationship: {}", e);
82            }
83        }
84
85        let value = result?;
86        serde_json::to_value(value).map_err(anyhow::Error::from)
87    }
88
89    /// Remove a relationship by ID
90    ///
91    /// This tool removes an existing episode relationship.
92    ///
93    /// # Arguments (from JSON)
94    ///
95    /// * `relationship_id` - UUID of the relationship to remove
96    pub async fn remove_episode_relationship_tool(&self, args: Value) -> Result<Value> {
97        debug!("Removing episode relationship with args: {}", args);
98
99        let input: RemoveEpisodeRelationshipInput = serde_json::from_value(args)?;
100        let relationship_id = input.relationship_id.clone();
101        let client_id = "mcp_client".to_string();
102
103        let tools = EpisodeRelationshipTools::new(self.memory());
104        let result = tools.remove_relationship(input).await;
105
106        // Log the operation to audit trail
107        let audit_logger = self.audit_logger();
108        match &result {
109            Ok(_) => {
110                audit_logger
111                    .log_remove_relationship(&client_id, &relationship_id, true)
112                    .await;
113
114                info!(relationship_id = %relationship_id, "Removed episode relationship via MCP");
115            }
116            Err(e) => {
117                audit_logger
118                    .log_remove_relationship(&client_id, &relationship_id, false)
119                    .await;
120
121                debug!("Failed to remove episode relationship: {}", e);
122            }
123        }
124
125        let value = result?;
126        serde_json::to_value(value).map_err(anyhow::Error::from)
127    }
128
129    /// Get relationships for an episode
130    ///
131    /// This tool retrieves all relationships for a given episode with optional
132    /// direction and type filtering.
133    ///
134    /// # Arguments (from JSON)
135    ///
136    /// * `episode_id` - Episode UUID to query
137    /// * `direction` - Optional direction filter ("outgoing", "incoming", "both")
138    /// * `relationship_type` - Optional relationship type filter
139    pub async fn get_episode_relationships_tool(&self, args: Value) -> Result<Value> {
140        debug!("Getting episode relationships with args: {}", args);
141
142        let input: GetEpisodeRelationshipsInput = serde_json::from_value(args)?;
143        let episode_id = input.episode_id.clone();
144        let client_id = "mcp_client".to_string();
145
146        let tools = EpisodeRelationshipTools::new(self.memory());
147        let result = tools.get_relationships(input).await;
148
149        // Log the operation to audit trail
150        let audit_logger = self.audit_logger();
151        match &result {
152            Ok(r) => {
153                let total_count = r.outgoing.len() + r.incoming.len();
154                audit_logger
155                    .log_get_relationships(&client_id, &episode_id, total_count, true)
156                    .await;
157
158                info!(
159                    episode_id = %episode_id,
160                    outgoing_count = r.outgoing.len(),
161                    incoming_count = r.incoming.len(),
162                    "Retrieved episode relationships via MCP"
163                );
164            }
165            Err(e) => {
166                audit_logger
167                    .log_get_relationships(&client_id, &episode_id, 0, false)
168                    .await;
169
170                debug!("Failed to get episode relationships: {}", e);
171            }
172        }
173
174        let value = result?;
175        serde_json::to_value(value).map_err(anyhow::Error::from)
176    }
177
178    /// Find episodes related to a given episode
179    ///
180    /// This tool finds all episodes related to the specified episode with
181    /// optional filtering by relationship type.
182    ///
183    /// # Arguments (from JSON)
184    ///
185    /// * `episode_id` - Episode UUID to find relationships for
186    /// * `relationship_type` - Optional relationship type filter
187    /// * `limit` - Optional maximum number of results (default: 10)
188    /// * `include_metadata` - Optional flag to include relationship metadata
189    pub async fn find_related_episodes_tool(&self, args: Value) -> Result<Value> {
190        debug!("Finding related episodes with args: {}", args);
191
192        let input: FindRelatedEpisodesInput = serde_json::from_value(args)?;
193        let episode_id = input.episode_id.clone();
194        let client_id = "mcp_client".to_string();
195
196        let tools = EpisodeRelationshipTools::new(self.memory());
197        let result = tools.find_related(input).await;
198
199        // Log the operation to audit trail
200        let audit_logger = self.audit_logger();
201        match &result {
202            Ok(r) => {
203                audit_logger
204                    .log_find_related(&client_id, &episode_id, r.count, true)
205                    .await;
206
207                info!(episode_id = %episode_id, related_count = r.count, "Found related episodes via MCP");
208            }
209            Err(e) => {
210                audit_logger
211                    .log_find_related(&client_id, &episode_id, 0, false)
212                    .await;
213
214                debug!("Failed to find related episodes: {}", e);
215            }
216        }
217
218        let value = result?;
219        serde_json::to_value(value).map_err(anyhow::Error::from)
220    }
221
222    /// Check if a specific relationship exists
223    ///
224    /// This tool checks whether a relationship of a specific type exists
225    /// between two episodes.
226    ///
227    /// # Arguments (from JSON)
228    ///
229    /// * `from_episode_id` - Source episode UUID
230    /// * `to_episode_id` - Target episode UUID
231    /// * `relationship_type` - Type of relationship to check
232    pub async fn check_relationship_exists_tool(&self, args: Value) -> Result<Value> {
233        debug!("Checking relationship exists with args: {}", args);
234
235        let input: CheckRelationshipExistsInput = serde_json::from_value(args)?;
236        let from_id = input.from_episode_id.clone();
237        let to_id = input.to_episode_id.clone();
238        let rel_type = input.relationship_type.clone();
239        let client_id = "mcp_client".to_string();
240
241        let tools = EpisodeRelationshipTools::new(self.memory());
242        let result = tools.check_exists(input).await;
243
244        // Log the operation to audit trail
245        let audit_logger = self.audit_logger();
246        match &result {
247            Ok(c) => {
248                audit_logger
249                    .log_check_relationship(&client_id, &from_id, &to_id, &rel_type, c.exists, true)
250                    .await;
251
252                info!(
253                    from_episode_id = %from_id,
254                    to_episode_id = %to_id,
255                    relationship_type = %rel_type,
256                    exists = c.exists,
257                    "Checked relationship existence via MCP"
258                );
259            }
260            Err(e) => {
261                audit_logger
262                    .log_check_relationship(&client_id, &from_id, &to_id, &rel_type, false, false)
263                    .await;
264
265                debug!("Failed to check relationship exists: {}", e);
266            }
267        }
268
269        let value = result?;
270        serde_json::to_value(value).map_err(anyhow::Error::from)
271    }
272
273    /// Get dependency graph for visualization
274    ///
275    /// This tool builds a relationship graph starting from an episode up to
276    /// a specified depth, optionally in DOT format for visualization.
277    ///
278    /// # Arguments (from JSON)
279    ///
280    /// * `episode_id` - Root episode UUID
281    /// * `depth` - Optional maximum traversal depth (1-5, default: 2)
282    /// * `format` - Optional output format ("json" or "dot", default: "json")
283    pub async fn get_dependency_graph_tool(&self, args: Value) -> Result<Value> {
284        debug!("Getting dependency graph with args: {}", args);
285
286        let input: DependencyGraphInput = serde_json::from_value(args)?;
287        let episode_id = input.episode_id.clone();
288        let format = input.format.clone().unwrap_or_else(|| "json".to_string());
289        let client_id = "mcp_client".to_string();
290
291        let tools = EpisodeRelationshipTools::new(self.memory());
292        let result = tools.get_dependency_graph(input).await;
293
294        // Log the operation to audit trail
295        let audit_logger = self.audit_logger();
296        match &result {
297            Ok(g) => {
298                audit_logger
299                    .log_dependency_graph(&client_id, &episode_id, g.node_count, g.edge_count, true)
300                    .await;
301
302                info!(
303                    episode_id = %episode_id,
304                    node_count = g.node_count,
305                    edge_count = g.edge_count,
306                    format = %format,
307                    "Retrieved dependency graph via MCP"
308                );
309            }
310            Err(e) => {
311                audit_logger
312                    .log_dependency_graph(&client_id, &episode_id, 0, 0, false)
313                    .await;
314
315                debug!("Failed to get dependency graph: {}", e);
316            }
317        }
318
319        let value = result?;
320        serde_json::to_value(value).map_err(anyhow::Error::from)
321    }
322
323    /// Validate that adding a relationship would not create a cycle
324    ///
325    /// This tool checks if adding a relationship between two episodes would
326    /// create a cycle in the dependency graph. Returns whether a cycle would
327    /// be created and the cycle path if detected.
328    ///
329    /// # Arguments (from JSON)
330    ///
331    /// * `from_episode_id` - Source episode UUID (proposed from)
332    /// * `to_episode_id` - Target episode UUID (proposed to)
333    /// * `relationship_type` - Type of relationship being added
334    pub async fn validate_no_cycles_tool(&self, args: Value) -> Result<Value> {
335        debug!("Validating no cycles with args: {}", args);
336
337        let input: ValidateNoCyclesInput = serde_json::from_value(args)?;
338        let from_id = input.from_episode_id.clone();
339        let to_id = input.to_episode_id.clone();
340        let rel_type = input.relationship_type.clone();
341        let client_id = "mcp_client".to_string();
342
343        let tools = EpisodeRelationshipTools::new(self.memory());
344        let result = tools.validate_no_cycles(input).await;
345
346        // Log the operation to audit trail
347        let audit_logger = self.audit_logger();
348        match &result {
349            Ok(v) => {
350                audit_logger
351                    .log_validate_cycles(
352                        &client_id,
353                        &from_id,
354                        &to_id,
355                        &rel_type,
356                        v.would_create_cycle,
357                        true,
358                    )
359                    .await;
360
361                info!(
362                    from_episode_id = %from_id,
363                    to_episode_id = %to_id,
364                    relationship_type = %rel_type,
365                    would_create_cycle = v.would_create_cycle,
366                    is_valid = v.is_valid,
367                    "Validated cycle absence via MCP"
368                );
369            }
370            Err(e) => {
371                audit_logger
372                    .log_validate_cycles(&client_id, &from_id, &to_id, &rel_type, false, false)
373                    .await;
374
375                debug!("Failed to validate no cycles: {}", e);
376            }
377        }
378
379        let value = result?;
380        serde_json::to_value(value).map_err(anyhow::Error::from)
381    }
382
383    /// Get topological ordering of episodes
384    ///
385    /// This tool returns episodes in topological order where dependencies come
386    /// before dependents. Only works on directed acyclic graphs (DAGs).
387    ///
388    /// # Arguments (from JSON)
389    ///
390    /// * `episode_ids` - Array of episode UUIDs to sort
391    pub async fn get_topological_order_tool(&self, args: Value) -> Result<Value> {
392        debug!("Getting topological order with args: {}", args);
393
394        let input: GetTopologicalOrderInput = serde_json::from_value(args)?;
395        let episode_count = input.episode_ids.len();
396        let client_id = "mcp_client".to_string();
397
398        let tools = EpisodeRelationshipTools::new(self.memory());
399        let result = tools.get_topological_order(input).await;
400
401        // Log the operation to audit trail
402        let audit_logger = self.audit_logger();
403        match &result {
404            Ok(o) => {
405                audit_logger
406                    .log_topological_order(&client_id, episode_count, o.count, o.has_cycles, true)
407                    .await;
408
409                info!(
410                    input_count = episode_count,
411                    output_count = o.count,
412                    has_cycles = o.has_cycles,
413                    "Computed topological order via MCP"
414                );
415            }
416            Err(e) => {
417                audit_logger
418                    .log_topological_order(&client_id, episode_count, 0, false, false)
419                    .await;
420
421                debug!("Failed to get topological order: {}", e);
422            }
423        }
424
425        let value = result?;
426        serde_json::to_value(value).map_err(anyhow::Error::from)
427    }
428}
429
430#[cfg(test)]
431mod tests;