subcog/mcp/tools/handlers/
groups.rs1use super::super::{ToolContent, ToolResult};
8use crate::mcp::tool_types::GroupsArgs;
9use crate::models::group::{GroupId, GroupRole};
10use crate::services::group::GroupService;
11use crate::{Error, Result};
12use serde::Deserialize;
13use serde_json::Value;
14
15fn parse_args<T: for<'de> Deserialize<'de>>(arguments: Value) -> Result<T> {
17 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))
18}
19
20#[derive(Debug, Deserialize)]
22struct GroupCreateArgs {
23 name: String,
24 description: Option<String>,
25}
26
27#[derive(Debug, Deserialize)]
29struct GroupGetArgs {
30 group_id: String,
31}
32
33#[derive(Debug, Deserialize)]
35struct GroupAddMemberArgs {
36 group_id: String,
37 user_id: String,
38 role: Option<String>,
39}
40
41#[derive(Debug, Deserialize)]
43struct GroupRemoveMemberArgs {
44 group_id: String,
45 user_id: String,
46}
47
48#[derive(Debug, Deserialize)]
50struct GroupUpdateRoleArgs {
51 group_id: String,
52 user_id: String,
53 role: String,
54}
55
56#[derive(Debug, Deserialize)]
58struct GroupDeleteArgs {
59 group_id: String,
60}
61
62fn get_user_id() -> String {
64 std::env::var("SUBCOG_USER_ID").unwrap_or_else(|_| "default-user".to_string())
65}
66
67fn get_org_id() -> String {
69 std::env::var("SUBCOG_ORG_ID").unwrap_or_else(|_| "default-org".to_string())
70}
71
72fn parse_role(role: Option<&str>) -> GroupRole {
74 match role.map(str::to_lowercase).as_deref() {
75 Some("admin") => GroupRole::Admin,
76 Some("write") => GroupRole::Write,
77 _ => GroupRole::Read,
78 }
79}
80
81fn text_result(text: String) -> ToolResult {
83 ToolResult {
84 content: vec![ToolContent::Text { text }],
85 is_error: false,
86 }
87}
88
89fn error_result(text: String) -> ToolResult {
91 ToolResult {
92 content: vec![ToolContent::Text { text }],
93 is_error: true,
94 }
95}
96
97const fn desc_or_none(desc: &str) -> &str {
99 if desc.is_empty() { "(none)" } else { desc }
100}
101
102pub fn execute_group_create(arguments: Value) -> Result<ToolResult> {
104 let args: GroupCreateArgs = parse_args(arguments)?;
105
106 let service = GroupService::try_default()?;
107 let user_id = get_user_id();
108 let org_id = get_org_id();
109
110 match service.create_group(
111 &org_id,
112 &args.name,
113 args.description.as_deref().unwrap_or(""),
114 &user_id,
115 ) {
116 Ok(group) => Ok(text_result(format!(
117 "## Group Created\n\n\
118 **ID:** {}\n\
119 **Name:** {}\n\
120 **Description:** {}\n\
121 **Your Role:** Admin\n\n\
122 You can now:\n\
123 - Add members with `subcog_group_add_member`\n\
124 - Capture memories to this group with `group_id` parameter",
125 group.id,
126 group.name,
127 desc_or_none(&group.description)
128 ))),
129 Err(e) => Ok(error_result(format!("Failed to create group: {e}"))),
130 }
131}
132
133pub fn execute_group_list(_arguments: Value) -> Result<ToolResult> {
135 let service = GroupService::try_default()?;
136 let user_id = get_user_id();
137 let org_id = get_org_id();
138
139 match service.get_user_groups(&org_id, &user_id) {
140 Ok(memberships) => {
141 if memberships.is_empty() {
142 return Ok(text_result(
143 "## Your Groups\n\n\
144 No groups found. Create one with `subcog_group_create`."
145 .to_string(),
146 ));
147 }
148
149 let mut output = String::from("## Your Groups\n\n");
150 for membership in memberships {
151 if let Ok(Some(group)) = service.get_group(&membership.group_id) {
153 output.push_str(&format!(
154 "### {}\n\
155 - **ID:** {}\n\
156 - **Description:** {}\n\
157 - **Your Role:** {}\n\n",
158 group.name,
159 group.id,
160 desc_or_none(&group.description),
161 membership.role.as_str(),
162 ));
163 }
164 }
165
166 Ok(text_result(output))
167 },
168 Err(e) => Ok(error_result(format!("Failed to list groups: {e}"))),
169 }
170}
171
172pub fn execute_group_get(arguments: Value) -> Result<ToolResult> {
174 let args: GroupGetArgs = parse_args(arguments)?;
175
176 let service = GroupService::try_default()?;
177 let group_id = GroupId::from(args.group_id.as_str());
178
179 match service.get_group(&group_id) {
180 Ok(Some(group)) => {
181 let members = service.list_members(&group_id).unwrap_or_default();
182
183 let mut output = format!(
184 "## Group: {}\n\n\
185 **ID:** {}\n\
186 **Description:** {}\n\
187 **Organization:** {}\n\n\
188 ### Members ({}):\n\n",
189 group.name,
190 group.id,
191 desc_or_none(&group.description),
192 group.org_id,
193 members.len()
194 );
195
196 for member in members {
197 output.push_str(&format!(
198 "- **{}** ({})\n",
199 member.email,
200 member.role.as_str()
201 ));
202 }
203
204 Ok(text_result(output))
205 },
206 Ok(None) => Ok(error_result(format!("Group not found: {}", args.group_id))),
207 Err(e) => Ok(error_result(format!("Failed to get group: {e}"))),
208 }
209}
210
211pub fn execute_group_add_member(arguments: Value) -> Result<ToolResult> {
213 let args: GroupAddMemberArgs = parse_args(arguments)?;
214
215 let service = GroupService::try_default()?;
216 let acting_user = get_user_id();
217 let group_id = GroupId::from(args.group_id.as_str());
218 let role = parse_role(args.role.as_deref());
219
220 match service.add_member(&group_id, &args.user_id, role, &acting_user) {
221 Ok(_member) => Ok(text_result(format!(
222 "## Member Added\n\n\
223 **User:** {}\n\
224 **Group:** {}\n\
225 **Role:** {}\n\n\
226 The user can now access group memories based on their role.",
227 args.user_id,
228 args.group_id,
229 role.as_str()
230 ))),
231 Err(e) => Ok(error_result(format!("Failed to add member: {e}"))),
232 }
233}
234
235pub fn execute_group_remove_member(arguments: Value) -> Result<ToolResult> {
237 let args: GroupRemoveMemberArgs = parse_args(arguments)?;
238
239 let service = GroupService::try_default()?;
240 let acting_user = get_user_id();
241 let group_id = GroupId::from(args.group_id.as_str());
242
243 match service.remove_member(&group_id, &args.user_id, &acting_user) {
244 Ok(removed) => {
245 if removed {
246 Ok(text_result(format!(
247 "## Member Removed\n\n\
248 **User:** {}\n\
249 **Group:** {}\n\n\
250 The user no longer has access to group memories.",
251 args.user_id, args.group_id
252 )))
253 } else {
254 Ok(error_result(format!(
255 "Member '{}' not found in group",
256 args.user_id
257 )))
258 }
259 },
260 Err(e) => Ok(error_result(format!("Failed to remove member: {e}"))),
261 }
262}
263
264pub fn execute_group_update_role(arguments: Value) -> Result<ToolResult> {
266 let args: GroupUpdateRoleArgs = parse_args(arguments)?;
267
268 let service = GroupService::try_default()?;
269 let acting_user = get_user_id();
270 let group_id = GroupId::from(args.group_id.as_str());
271 let role = parse_role(Some(&args.role));
272
273 match service.update_member_role(&group_id, &args.user_id, role, &acting_user) {
274 Ok(updated) => {
275 if updated {
276 Ok(text_result(format!(
277 "## Role Updated\n\n\
278 **User:** {}\n\
279 **Group:** {}\n\
280 **New Role:** {}",
281 args.user_id,
282 args.group_id,
283 role.as_str()
284 )))
285 } else {
286 Ok(error_result(format!(
287 "Member '{}' not found in group",
288 args.user_id
289 )))
290 }
291 },
292 Err(e) => Ok(error_result(format!("Failed to update role: {e}"))),
293 }
294}
295
296pub fn execute_group_delete(arguments: Value) -> Result<ToolResult> {
298 let args: GroupDeleteArgs = parse_args(arguments)?;
299
300 let service = GroupService::try_default()?;
301 let acting_user = get_user_id();
302 let group_id = GroupId::from(args.group_id.as_str());
303
304 match service.delete_group(&group_id, &acting_user) {
305 Ok(deleted) => {
306 if deleted {
307 Ok(text_result(format!(
308 "## Group Deleted\n\n\
309 **Group ID:** {}\n\n\
310 The group has been deleted. Existing memories remain but are no longer group-accessible.",
311 args.group_id
312 )))
313 } else {
314 Ok(error_result(format!("Group not found: {}", args.group_id)))
315 }
316 },
317 Err(e) => Ok(error_result(format!("Failed to delete group: {e}"))),
318 }
319}
320
321pub fn execute_groups(arguments: Value) -> Result<ToolResult> {
330 let args: GroupsArgs =
331 serde_json::from_value(arguments).map_err(|e| Error::InvalidInput(e.to_string()))?;
332
333 match args.action.as_str() {
334 "create" => execute_groups_create(&args),
335 "list" => execute_groups_list(),
336 "get" => execute_groups_get(&args),
337 "add_member" => execute_groups_add_member(&args),
338 "remove_member" => execute_groups_remove_member(&args),
339 "update_role" => execute_groups_update_role(&args),
340 "delete" => execute_groups_delete(&args),
341 _ => Err(Error::InvalidInput(format!(
342 "Unknown groups action: '{}'. Valid actions: create, list, get, add_member, remove_member, update_role, delete",
343 args.action
344 ))),
345 }
346}
347
348fn execute_groups_create(args: &GroupsArgs) -> Result<ToolResult> {
350 let name = args
351 .name
352 .as_ref()
353 .ok_or_else(|| Error::InvalidInput("'name' is required for create action".to_string()))?;
354
355 let service = GroupService::try_default()?;
356 let user_id = get_user_id();
357 let org_id = get_org_id();
358
359 match service.create_group(
360 &org_id,
361 name,
362 args.description.as_deref().unwrap_or(""),
363 &user_id,
364 ) {
365 Ok(group) => Ok(text_result(format!(
366 "## Group Created\n\n\
367 **ID:** {}\n\
368 **Name:** {}\n\
369 **Description:** {}\n\
370 **Your Role:** Admin\n\n\
371 You can now:\n\
372 - Add members with `subcog_groups` action=add_member\n\
373 - Capture memories to this group with `group_id` parameter",
374 group.id,
375 group.name,
376 desc_or_none(&group.description)
377 ))),
378 Err(e) => Ok(error_result(format!("Failed to create group: {e}"))),
379 }
380}
381
382fn execute_groups_list() -> Result<ToolResult> {
384 let service = GroupService::try_default()?;
385 let user_id = get_user_id();
386 let org_id = get_org_id();
387
388 match service.get_user_groups(&org_id, &user_id) {
389 Ok(memberships) => {
390 if memberships.is_empty() {
391 return Ok(text_result(
392 "## Your Groups\n\n\
393 No groups found. Create one with `subcog_groups` action=create."
394 .to_string(),
395 ));
396 }
397
398 let mut output = String::from("## Your Groups\n\n");
399 for membership in memberships {
400 if let Ok(Some(group)) = service.get_group(&membership.group_id) {
402 output.push_str(&format!(
403 "### {}\n\
404 - **ID:** {}\n\
405 - **Description:** {}\n\
406 - **Your Role:** {}\n\n",
407 group.name,
408 group.id,
409 desc_or_none(&group.description),
410 membership.role.as_str(),
411 ));
412 }
413 }
414
415 Ok(text_result(output))
416 },
417 Err(e) => Ok(error_result(format!("Failed to list groups: {e}"))),
418 }
419}
420
421fn execute_groups_get(args: &GroupsArgs) -> Result<ToolResult> {
423 let group_id_str = args
424 .group_id
425 .as_ref()
426 .ok_or_else(|| Error::InvalidInput("'group_id' is required for get action".to_string()))?;
427
428 let service = GroupService::try_default()?;
429 let group_id = GroupId::from(group_id_str.as_str());
430
431 match service.get_group(&group_id) {
432 Ok(Some(group)) => {
433 let members = service.list_members(&group_id).unwrap_or_default();
434
435 let mut output = format!(
436 "## Group: {}\n\n\
437 **ID:** {}\n\
438 **Description:** {}\n\
439 **Organization:** {}\n\n\
440 ### Members ({}):\n\n",
441 group.name,
442 group.id,
443 desc_or_none(&group.description),
444 group.org_id,
445 members.len()
446 );
447
448 for member in members {
449 output.push_str(&format!(
450 "- **{}** ({})\n",
451 member.email,
452 member.role.as_str()
453 ));
454 }
455
456 Ok(text_result(output))
457 },
458 Ok(None) => Ok(error_result(format!("Group not found: {group_id_str}"))),
459 Err(e) => Ok(error_result(format!("Failed to get group: {e}"))),
460 }
461}
462
463fn execute_groups_add_member(args: &GroupsArgs) -> Result<ToolResult> {
465 let group_id_str = args.group_id.as_ref().ok_or_else(|| {
466 Error::InvalidInput("'group_id' is required for add_member action".to_string())
467 })?;
468
469 let user_id = args.user_id.as_ref().ok_or_else(|| {
470 Error::InvalidInput("'user_id' is required for add_member action".to_string())
471 })?;
472
473 let service = GroupService::try_default()?;
474 let acting_user = get_user_id();
475 let group_id = GroupId::from(group_id_str.as_str());
476 let role = parse_role(args.role.as_deref());
477
478 match service.add_member(&group_id, user_id, role, &acting_user) {
479 Ok(_member) => Ok(text_result(format!(
480 "## Member Added\n\n\
481 **User:** {}\n\
482 **Group:** {}\n\
483 **Role:** {}\n\n\
484 The user can now access group memories based on their role.",
485 user_id,
486 group_id_str,
487 role.as_str()
488 ))),
489 Err(e) => Ok(error_result(format!("Failed to add member: {e}"))),
490 }
491}
492
493fn execute_groups_remove_member(args: &GroupsArgs) -> Result<ToolResult> {
495 let group_id_str = args.group_id.as_ref().ok_or_else(|| {
496 Error::InvalidInput("'group_id' is required for remove_member action".to_string())
497 })?;
498
499 let user_id = args.user_id.as_ref().ok_or_else(|| {
500 Error::InvalidInput("'user_id' is required for remove_member action".to_string())
501 })?;
502
503 let service = GroupService::try_default()?;
504 let acting_user = get_user_id();
505 let group_id = GroupId::from(group_id_str.as_str());
506
507 match service.remove_member(&group_id, user_id, &acting_user) {
508 Ok(removed) => {
509 if removed {
510 Ok(text_result(format!(
511 "## Member Removed\n\n\
512 **User:** {user_id}\n\
513 **Group:** {group_id_str}\n\n\
514 The user no longer has access to group memories."
515 )))
516 } else {
517 Ok(error_result(format!(
518 "Member '{user_id}' not found in group"
519 )))
520 }
521 },
522 Err(e) => Ok(error_result(format!("Failed to remove member: {e}"))),
523 }
524}
525
526fn execute_groups_update_role(args: &GroupsArgs) -> Result<ToolResult> {
528 let group_id_str = args.group_id.as_ref().ok_or_else(|| {
529 Error::InvalidInput("'group_id' is required for update_role action".to_string())
530 })?;
531
532 let user_id = args.user_id.as_ref().ok_or_else(|| {
533 Error::InvalidInput("'user_id' is required for update_role action".to_string())
534 })?;
535
536 let role_str = args.role.as_ref().ok_or_else(|| {
537 Error::InvalidInput("'role' is required for update_role action".to_string())
538 })?;
539
540 let service = GroupService::try_default()?;
541 let acting_user = get_user_id();
542 let group_id = GroupId::from(group_id_str.as_str());
543 let role = parse_role(Some(role_str));
544
545 match service.update_member_role(&group_id, user_id, role, &acting_user) {
546 Ok(updated) => {
547 if updated {
548 Ok(text_result(format!(
549 "## Role Updated\n\n\
550 **User:** {}\n\
551 **Group:** {}\n\
552 **New Role:** {}",
553 user_id,
554 group_id_str,
555 role.as_str()
556 )))
557 } else {
558 Ok(error_result(format!(
559 "Member '{user_id}' not found in group"
560 )))
561 }
562 },
563 Err(e) => Ok(error_result(format!("Failed to update role: {e}"))),
564 }
565}
566
567fn execute_groups_delete(args: &GroupsArgs) -> Result<ToolResult> {
569 let group_id_str = args.group_id.as_ref().ok_or_else(|| {
570 Error::InvalidInput("'group_id' is required for delete action".to_string())
571 })?;
572
573 let service = GroupService::try_default()?;
574 let acting_user = get_user_id();
575 let group_id = GroupId::from(group_id_str.as_str());
576
577 match service.delete_group(&group_id, &acting_user) {
578 Ok(deleted) => {
579 if deleted {
580 Ok(text_result(format!(
581 "## Group Deleted\n\n\
582 **Group ID:** {group_id_str}\n\n\
583 The group has been deleted. Existing memories remain but are no longer group-accessible."
584 )))
585 } else {
586 Ok(error_result(format!("Group not found: {group_id_str}")))
587 }
588 },
589 Err(e) => Ok(error_result(format!("Failed to delete group: {e}"))),
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn test_parse_role() {
599 assert_eq!(parse_role(Some("admin")), GroupRole::Admin);
600 assert_eq!(parse_role(Some("ADMIN")), GroupRole::Admin);
601 assert_eq!(parse_role(Some("write")), GroupRole::Write);
602 assert_eq!(parse_role(Some("read")), GroupRole::Read);
603 assert_eq!(parse_role(None), GroupRole::Read);
604 assert_eq!(parse_role(Some("invalid")), GroupRole::Read);
605 }
606
607 #[test]
608 fn test_text_result() {
609 let result = text_result("test".to_string());
610 assert!(!result.is_error);
611 assert_eq!(result.content.len(), 1);
612 }
613
614 #[test]
615 fn test_error_result() {
616 let result = error_result("error".to_string());
617 assert!(result.is_error);
618 assert_eq!(result.content.len(), 1);
619 }
620}