refactor: drop Manage prefix from consolidated smart MCP tools

This commit is contained in:
Riz Ashraf committed 2026-10-05 11:11:41 +01:00
1 parent e45db33297
commit 961abb01e7
8 files changed
+914 -100

No files matched your search

+50
View File
@@ -162,6 +162,56 @@ impl McpTool for GetEnvironmentDetailsHandler {
}
}
pub struct EnvironmentHandler;
#[async_trait]
impl McpTool for EnvironmentHandler {
fn name(&self) -> &'static str {
"environment"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<EnvironmentTool>(
"environment",
"Consolidated environment & infrastructure management",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
EnvAction::UpdateFingerprint => {
let tool_versions = req.tool_versions.unwrap_or_default();
UpdateEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns, "tool_versions": tool_versions}), state).await
}
EnvAction::ReadFingerprint => {
ReadEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns}), state).await
}
EnvAction::LogRequirement => {
let key = req.key.ok_or_else(|| {
crate::error::AppError::Internal("key is required for 'log_requirement' action".to_string())
})?;
let desc = req.description.unwrap_or_default();
let secret = req.is_secret.unwrap_or(false);
LogEnvRequirementHandler.execute(serde_json::json!({"key": key, "description": desc, "is_secret": secret, "namespace": ns}), state).await
}
EnvAction::Register => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("name is required for 'register' action".to_string())
})?;
let url = req.url.unwrap_or_default();
let desc = req.description.unwrap_or_default();
let vpn = req.requires_vpn.unwrap_or(false);
RegisterEnvironmentHandler.execute(serde_json::json!({"name": name, "url": url, "description": desc, "requires_vpn": vpn, "namespace": ns}), state).await
}
EnvAction::GetDetails => {
GetEnvironmentDetailsHandler.execute(serde_json::json!({"namespace": ns}), state).await
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+103
View File
@@ -524,6 +524,109 @@ impl McpTool for ListTechDebtHandler {
}
}
pub struct DecisionsHandler;
#[async_trait]
impl McpTool for DecisionsHandler {
fn name(&self) -> &'static str {
"decisions"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<DecisionsTool>(
"decisions",
"Consolidated Architectural Decision Records (ADRs) management (log, query, delete)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
DecisionAction::Log => {
let title = req.title.ok_or_else(|| {
crate::error::AppError::Internal("title is required for 'log' action".to_string())
})?;
let status = req.status.unwrap_or_else(|| "accepted".to_string());
let context = req.context.unwrap_or_default();
let decision = req.decision.unwrap_or_default();
let consequences = req.consequences.unwrap_or_default();
LogDecisionHandler.execute(
serde_json::json!({
"title": title,
"status": status,
"context": context,
"decision": decision,
"consequences": consequences,
"namespace": ns
}),
state,
).await
}
DecisionAction::Query => {
QueryDecisionsHandler.execute(serde_json::json!({"query": req.query, "namespace": ns}), state).await
}
DecisionAction::Delete => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'delete' action".to_string())
})?;
DeleteDecisionHandler.execute(serde_json::json!({"id": id}), state).await
}
}
}
}
pub struct TechDebtHandler;
#[async_trait]
impl McpTool for TechDebtHandler {
fn name(&self) -> &'static str {
"tech_debt"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<TechDebtTool>(
"tech_debt",
"Consolidated technical debt management (log, resolve, list)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
TechDebtAction::Log => {
let desc = req.description.ok_or_else(|| {
crate::error::AppError::Internal("description is required for 'log' action".to_string())
})?;
let ideal = req.ideal_solution.unwrap_or_default();
LogTechDebtHandler.execute(
serde_json::json!({
"description": desc,
"ideal_solution": ideal,
"git_commit": req.git_commit,
"git_branch": req.git_branch,
"symbol_references": req.symbol_references,
"line_range": req.line_range,
"namespace": ns
}),
state,
).await
}
TechDebtAction::Resolve => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'resolve' action".to_string())
})?;
ResolveTechDebtHandler.execute(serde_json::json!({"id": id}), state).await
}
TechDebtAction::List => {
let inc = req.include_resolved.unwrap_or(false);
ListTechDebtHandler.execute(serde_json::json!({"include_resolved": inc, "namespace": ns}), state).await
}
}
}
}
pub struct OmniSearchHandler;
#[async_trait]
+68 -94
View File
@@ -7,23 +7,23 @@ use serde_json::Value;
use std::collections::HashSet;
use std::sync::Arc;
pub struct ManageStickyNotesHandler;
pub struct StickyNotesHandler;
#[async_trait]
impl McpTool for ManageStickyNotesHandler {
impl McpTool for StickyNotesHandler {
fn name(&self) -> &'static str {
"manage_sticky_notes"
"sticky_notes"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ManageStickyNotesTool>(
"manage_sticky_notes",
"Manage ephemeral sticky notes with TTL (add, read, delete, clear)",
crate::mcp::tool_def::<StickyNotesTool>(
"sticky_notes",
"Ephemeral sticky notes with TTL (add, read, delete, clear)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ManageStickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: StickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
StickyNoteAction::Add => {
let content = req.content.ok_or_else(|| {
@@ -216,97 +216,72 @@ impl McpTool for ClearStickyNotesHandler {
}
}
pub struct LeaveHandoffMemoHandler;
pub struct HandoffMemosHandler;
#[async_trait]
impl McpTool for LeaveHandoffMemoHandler {
impl McpTool for HandoffMemosHandler {
fn name(&self) -> &'static str {
"leave_handoff_memo"
"handoff_memos"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<LeaveHandoffMemoTool>(
"leave_handoff_memo",
"Execute leave_handoff_memo",
crate::mcp::tool_def::<HandoffMemosTool>(
"handoff_memos",
"Handoff memos across sessions (leave, read, clear)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LeaveHandoffMemoTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.telemetry.handoff_memos.modify(|memos| {
memos.push(crate::models::HandoffMemo {
id: uuid::Uuid::new_v4().to_string(),
author: "agy".to_string(),
content: req.content,
namespace: req.namespace,
timestamp: crate::handlers::utils::now_secs(),
expires_at: None,
});
if memos.len() > 200 {
memos.remove(0);
}
});
Ok("Handoff memo left".to_string())
}
}
pub struct ReadHandoffMemosHandler;
#[async_trait]
impl McpTool for ReadHandoffMemosHandler {
fn name(&self) -> &'static str {
"read_handoff_memos"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ReadHandoffMemosTool>(
"read_handoff_memos",
"Execute read_handoff_memos",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.telemetry.handoff_memos.read_with(|items| {
let filtered: Vec<_> = items
.iter()
.filter(|i| {
if let Some(ns) = &req.namespace {
&i.namespace == ns
} else {
true
let req: HandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
HandoffMemoAction::Leave => {
let content = req.content.ok_or_else(|| {
crate::error::AppError::Internal("content is required for 'leave' action".to_string())
})?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
state.telemetry.handoff_memos.modify(|memos| {
memos.push(crate::models::HandoffMemo {
id: uuid::Uuid::new_v4().to_string(),
author: "agy".to_string(),
content,
namespace: ns,
timestamp: crate::handlers::utils::now_secs(),
expires_at: None,
});
if memos.len() > 200 {
memos.remove(0);
}
})
.collect();
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?;
Ok(data)
}
}
pub struct ClearHandoffMemosHandler;
#[async_trait]
impl McpTool for ClearHandoffMemosHandler {
fn name(&self) -> &'static str {
"clear_handoff_memos"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ClearHandoffMemosTool>(
"clear_handoff_memos",
"Execute clear_handoff_memos",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ClearHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ids: HashSet<_> = req.ids.into_iter().collect();
state
.telemetry
.handoff_memos
.modify(|memos| memos.retain(|m| !ids.contains(&m.id)));
Ok("Handoff memos cleared".to_string())
});
Ok("Handoff memo left.".to_string())
}
HandoffMemoAction::Read => {
let data = state.telemetry.handoff_memos.read_with(|items| {
let filtered: Vec<_> = items
.iter()
.filter(|i| {
if let Some(ns) = &req.namespace {
&i.namespace == ns
} else {
true
}
})
.collect();
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?;
Ok(data)
}
HandoffMemoAction::Clear => {
let ids: HashSet<_> = req.ids.unwrap_or_default().into_iter().collect();
state.telemetry.handoff_memos.modify(|memos| {
if ids.is_empty() {
memos.clear();
} else {
memos.retain(|m| !ids.contains(&m.id));
}
});
Ok("Handoff memos cleared.".to_string())
}
}
}
}
@@ -481,10 +456,10 @@ mod tests {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handoff_handler = LeaveHandoffMemoHandler;
let handoff_handler = HandoffMemosHandler;
let args = json!({
"action": "leave",
"content": "Finished implementing graph tests",
"author": "Antigravity",
"namespace": "global"
});
@@ -493,11 +468,10 @@ mod tests {
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res, "Handoff memo left");
assert_eq!(res, "Handoff memo left.");
let read_handoff = ReadHandoffMemosHandler;
let res2 = read_handoff
.execute(json!({"namespace": "global"}), state.clone())
let res2 = handoff_handler
.execute(json!({"action": "read", "namespace": "global"}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -532,7 +506,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = ManageStickyNotesHandler;
let handler = StickyNotesHandler;
let invalid_args = serde_json::json!({
"action": "invalid_action"
});
+139
View File
@@ -512,6 +512,145 @@ impl McpTool for ListMilestonesHandler {
}
}
pub struct TasksHandler;
#[async_trait]
impl McpTool for TasksHandler {
fn name(&self) -> &'static str {
"tasks"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<TasksTool>(
"tasks",
"Consolidated task management (add, update, delete, list, set_criteria, verify)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
TaskAction::Add => {
let title = req.title.ok_or_else(|| {
crate::error::AppError::Internal("title is required for 'add' action".to_string())
})?;
let description = req.description.unwrap_or_default();
let add_args = serde_json::json!({
"title": title,
"description": description,
"git_branch": req.git_branch,
"parent_id": req.parent_id,
"dependencies": req.dependencies,
});
AddTaskHandler.execute(add_args, state).await
}
TaskAction::Update => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'update' action".to_string())
})?;
let status = req.status.ok_or_else(|| {
crate::error::AppError::Internal("status is required for 'update' action".to_string())
})?;
let update_args = serde_json::json!({
"id": id,
"status": status,
});
UpdateTaskStatusHandler.execute(update_args, state).await
}
TaskAction::Delete => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'delete' action".to_string())
})?;
let del_args = serde_json::json!({
"id": id,
});
DeleteTaskHandler.execute(del_args, state).await
}
TaskAction::List => {
let list_args = serde_json::json!({
"git_branch": req.git_branch,
"summary_level": req.summary_level,
"max_tokens": req.max_tokens,
});
ListActiveTasksHandler.execute(list_args, state).await
}
TaskAction::SetCriteria => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'set_criteria' action".to_string())
})?;
let criteria = req.criteria.ok_or_else(|| {
crate::error::AppError::Internal("criteria array is required for 'set_criteria' action".to_string())
})?;
let set_args = serde_json::json!({
"id": id,
"acceptance_criteria": criteria,
});
SetAcceptanceCriteriaHandler.execute(set_args, state).await
}
TaskAction::Verify => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'verify' action".to_string())
})?;
let verify_args = serde_json::json!({
"id": id,
});
VerifyAcceptanceCriteriaHandler.execute(verify_args, state).await
}
}
}
}
pub struct MilestonesHandler;
#[async_trait]
impl McpTool for MilestonesHandler {
fn name(&self) -> &'static str {
"milestones"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<MilestonesTool>(
"milestones",
"Consolidated milestone management (add, update, list)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
MilestoneAction::Add => {
let title = req.title.ok_or_else(|| {
crate::error::AppError::Internal("title is required for 'add' action".to_string())
})?;
let add_args = serde_json::json!({
"title": title,
"namespace": req.namespace.unwrap_or_else(|| crate::models::default_namespace()),
});
AddMilestoneHandler.execute(add_args, state).await
}
MilestoneAction::Update => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("id is required for 'update' action".to_string())
})?;
let status = req.status.ok_or_else(|| {
crate::error::AppError::Internal("status is required for 'update' action".to_string())
})?;
let update_args = serde_json::json!({
"id": id,
"status": status,
});
UpdateMilestoneHandler.execute(update_args, state).await
}
MilestoneAction::List => {
let list_args = serde_json::json!({
"namespace": req.namespace,
});
ListMilestonesHandler.execute(list_args, state).await
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+42
View File
@@ -218,6 +218,48 @@ impl McpTool for ToggleClipboardWatchModeHandler {
}
}
use crate::tools::{ClipboardAction, ClipboardTool};
pub struct ClipboardHandler;
#[async_trait]
impl McpTool for ClipboardHandler {
fn name(&self) -> &'static str {
"clipboard"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ClipboardTool>(
"clipboard",
"Consolidated OS clipboard management (read, write, toggle_watch)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
ClipboardAction::Read => {
ReadClipboardHandler.execute(serde_json::json!({}), state).await
}
ClipboardAction::Write => {
WriteClipboardHandler.execute(
serde_json::json!({
"text": req.text,
"html": req.html,
"files": req.files,
"image_path": req.image_path
}),
state,
).await
}
ClipboardAction::ToggleWatch => {
let enable = req.enabled.unwrap_or(true);
ToggleClipboardWatchModeHandler.execute(serde_json::json!({"enable": enable}), state).await
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+197
View File
@@ -475,6 +475,203 @@ impl McpTool for ClearPrChecklistHandler {
}
}
pub struct PinnedFilesHandler;
#[async_trait]
impl McpTool for PinnedFilesHandler {
fn name(&self) -> &'static str {
"pinned_files"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<PinnedFilesTool>(
"pinned_files",
"Consolidated pinned files focus management (pin, unpin, list)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
PinnedFileAction::Pin => {
let path = req.path.ok_or_else(|| {
crate::error::AppError::Internal("path is required for 'pin' action".to_string())
})?;
PinFileHandler.execute(serde_json::json!({"file_path": path, "namespace": ns}), state).await
}
PinnedFileAction::Unpin => {
let path = req.path.ok_or_else(|| {
crate::error::AppError::Internal("path is required for 'unpin' action".to_string())
})?;
UnpinFileHandler.execute(serde_json::json!({"file_path": path, "namespace": ns}), state).await
}
PinnedFileAction::List => {
ListPinnedFilesHandler.execute(serde_json::json!({"namespace": ns}), state).await
}
}
}
}
pub struct ContextWorkspacesHandler;
#[async_trait]
impl McpTool for ContextWorkspacesHandler {
fn name(&self) -> &'static str {
"context_workspaces"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<ContextWorkspacesTool>(
"context_workspaces",
"Consolidated context workspace state management (save, load, list, delete, diff)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ContextWorkspacesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
ContextWorkspaceAction::Save => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("name is required for 'save' action".to_string())
})?;
let pinned_files = req.pinned_files.unwrap_or_default();
let active_task_ids = req.active_task_ids.unwrap_or_default();
SaveContextWorkspaceHandler.execute(
serde_json::json!({
"name": name,
"namespace": ns,
"pinned_files": pinned_files,
"active_task_ids": active_task_ids
}),
state,
).await
}
ContextWorkspaceAction::Load => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("name is required for 'load' action".to_string())
})?;
LoadContextWorkspaceHandler.execute(serde_json::json!({"name": name, "namespace": ns}), state).await
}
ContextWorkspaceAction::List => {
ListContextWorkspacesHandler.execute(serde_json::json!({"namespace": ns}), state).await
}
ContextWorkspaceAction::Delete => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("name is required for 'delete' action".to_string())
})?;
DeleteContextWorkspaceHandler.execute(serde_json::json!({"name": name, "namespace": ns}), state).await
}
ContextWorkspaceAction::Diff => {
let name = req.name.ok_or_else(|| {
crate::error::AppError::Internal("name is required for 'diff' action".to_string())
})?;
let other = req.other_name.ok_or_else(|| {
crate::error::AppError::Internal("other_name is required for 'diff' action".to_string())
})?;
DiffContextWorkspacesHandler.execute(serde_json::json!({"workspace_a": name, "workspace_b": other, "namespace": ns}), state).await
}
}
}
}
pub struct PrChecklistHandler;
#[async_trait]
impl McpTool for PrChecklistHandler {
fn name(&self) -> &'static str {
"pr_checklist"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<PrChecklistTool>(
"pr_checklist",
"Consolidated PR checklist management (add, get, clear)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
match req.action {
PrChecklistAction::Add => {
let desc = req.description.ok_or_else(|| {
crate::error::AppError::Internal("description is required for 'add' action".to_string())
})?;
AddPrChecklistItemHandler.execute(serde_json::json!({"description": desc, "namespace": ns}), state).await
}
PrChecklistAction::Get => {
GetPrChecklistHandler.execute(serde_json::json!({"namespace": ns}), state).await
}
PrChecklistAction::Clear => {
ClearPrChecklistHandler.execute(serde_json::json!({"namespace": ns}), state).await
}
}
}
}
pub struct SnippetsHandler;
#[async_trait]
impl McpTool for SnippetsHandler {
fn name(&self) -> &'static str {
"snippets"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<SnippetsTool>(
"snippets",
"Consolidated reusable code snippets management (store, search, delete, tag)",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action {
SnippetAction::Store => {
let name = req.query.or(req.id).ok_or_else(|| {
crate::error::AppError::Internal("name is required for 'store' action".to_string())
})?;
let lang = req.language.unwrap_or_else(|| "text".to_string());
let code = req.code.unwrap_or_default();
let desc = req.description.unwrap_or_default();
StoreSnippetHandler.execute(
serde_json::json!({
"name": name,
"language": lang,
"code": code,
"description": desc,
"tags": req.tags
}),
state,
).await
}
SnippetAction::Search => {
let q = req.query.unwrap_or_default();
if req.hybrid.unwrap_or(false) {
crate::handlers::meta::SearchSnippetsHybridHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await
} else {
SearchSnippetsHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await
}
}
SnippetAction::Delete => {
let id = req.id.or(req.query).ok_or_else(|| {
crate::error::AppError::Internal("id/name is required for 'delete' action".to_string())
})?;
DeleteSnippetHandler.execute(serde_json::json!({"name": id}), state).await
}
SnippetAction::Tag => {
let id = req.id.or(req.query).ok_or_else(|| {
crate::error::AppError::Internal("id/name is required for 'tag' action".to_string())
})?;
let tags = req.tags.unwrap_or_default();
TagSnippetHandler.execute(serde_json::json!({"name": id, "tags": tags}), state).await
}
}
}
}
use crate::handlers::utils::*;
#[cfg(test)]