1- from fastapi import APIRouter , HTTPException , Body
1+ from fastapi import APIRouter , HTTPException , Body , Query
22from app .models .actions import ActionsResponse , Agent , Rule , MCP
33from app .services .actions_loader import actions_loader
44from app .services .mcp_installer import get_agent_content , get_rule_content , create_mcp_config
5- from typing import List , Dict , Any
5+ from app .services .search_service import search_service
6+ from typing import List , Dict , Any , Optional
67import json
78
8- router = APIRouter (prefix = "/api/actions " , tags = ["actions" ])
9+ router = APIRouter (prefix = "/api" , tags = ["actions" ])
910
10- @router .get ("/" , response_model = ActionsResponse )
11+ @router .get ("/actions " , response_model = ActionsResponse , operation_id = "get_all_actions_endpoint" )
1112async def get_all_actions ():
1213 """Get all available actions (agents, rules, MCPs)"""
1314 return ActionsResponse (
@@ -16,80 +17,52 @@ async def get_all_actions():
1617 mcps = actions_loader .get_mcps ()
1718 )
1819
19- @router .get ("/agents" , response_model = List [ Agent ] )
20+ @router .get ("/agents" , operation_id = "get_agents_endpoint" )
2021async def get_agents ():
21- """Get all available agents"""
22- return actions_loader .get_agents ()
22+ """Get all available agents with tags only"""
23+ agents = actions_loader .get_agents ()
24+ return [
25+ {
26+ "name" : agent .name ,
27+ "display_name" : agent .display_name ,
28+ "slug" : agent .slug ,
29+ "tags" : agent .tags ,
30+ "filename" : agent .filename
31+ }
32+ for agent in agents
33+ ]
2334
24- @router .get ("/rules" , response_model = List [ Rule ] )
35+ @router .get ("/rules" , operation_id = "get_rules_endpoint" )
2536async def get_rules ():
26- """Get all available rules"""
27- return actions_loader .get_rules ()
37+ """Get all available rules with tags only"""
38+ rules = actions_loader .get_rules ()
39+ return [
40+ {
41+ "name" : rule .name ,
42+ "display_name" : rule .display_name ,
43+ "slug" : rule .slug ,
44+ "tags" : rule .tags ,
45+ "filename" : rule .filename
46+ }
47+ for rule in rules
48+ ]
2849
29- @router .get ("/mcps" , response_model = List [ MCP ] )
50+ @router .get ("/mcps" , operation_id = "get_mcps_endpoint" )
3051async def get_mcps ():
31- """Get all available MCPs"""
32- return actions_loader .get_mcps ()
52+ """Get all available MCPs with tags only"""
53+ mcps = actions_loader .get_mcps ()
54+ return [
55+ {
56+ "name" : mcp .name ,
57+ "tags" : mcp .tags if hasattr (mcp , 'tags' ) else []
58+ }
59+ for mcp in mcps
60+ ]
3361
34- @router .get ("/agent-content/{agent_id}" )
35- async def get_agent_content_endpoint (agent_id : str ):
36- """Get agent content for virtual workspace"""
37- agents = actions_loader .get_agents ()
38- # Match by slug first, fallback to name for backward compat
39- agent = next ((a for a in agents if (a .slug == agent_id or a .name == agent_id )), None )
40-
41- if not agent :
42- raise HTTPException (status_code = 404 , detail = f"Agent not found: { agent_id } " )
43-
44- # Get content directly from the agent object (already loaded from consolidated file)
45- content = agent .content
46- if not content :
47- raise HTTPException (status_code = 500 , detail = "Agent has no content" )
48-
49- return {
50- "filename" : agent .filename ,
51- "content" : content ,
52- "path" : f".claude/agents/{ agent .filename } "
53- }
5462
55- @router .get ("/rule-content/{rule_id}" )
56- async def get_rule_content_endpoint (rule_id : str ):
57- """Get rule content to append to CLAUDE.md"""
58- rules = actions_loader .get_rules ()
59- # Match by slug first, fallback to name for backward compat
60- rule = next ((r for r in rules if (r .slug == rule_id or r .name == rule_id )), None )
61-
62- if not rule :
63- raise HTTPException (status_code = 404 , detail = f"Rule not found: { rule_id } " )
64-
65- # Get content directly from the rule object (already loaded from consolidated file)
66- content = rule .content
67- if not content :
68- raise HTTPException (status_code = 500 , detail = "Rule has no content" )
69-
70- return {
71- "content" : content .strip ()
72- }
7363
74- @router .post ("/mcp-config/{mcp_name}" )
75- async def get_mcp_config_endpoint (mcp_name : str , current_config : Dict [str , Any ] = Body (default = {})):
76- """Get updated MCP config for virtual workspace"""
77- mcps = actions_loader .get_mcps ()
78- mcp = next ((m for m in mcps if m .name == mcp_name ), None )
79-
80- if not mcp :
81- raise HTTPException (status_code = 404 , detail = "MCP not found" )
82-
83- updated_config , was_removed = create_mcp_config (current_config , mcp .name , mcp .config )
84-
85- return {
86- "filename" : ".mcp.json" ,
87- "content" : updated_config ,
88- "path" : ".mcp.json" ,
89- "was_removed" : was_removed
90- }
9164
92- @router .get ("/merged-block" )
65+ @router .get ("/merged-block" , operation_id = "get_merged_actions_block_endpoint" )
9366async def get_merged_actions_block ():
9467 """Get all actions merged into a single block with metadata for frontend"""
9568 agents = actions_loader .get_agents ()
@@ -125,4 +98,132 @@ async def get_merged_actions_block():
12598 ]
12699 }
127100
128- return merged
101+ return merged
102+
103+ @router .get ("/search/agents" , tags = ["mcp" ], operation_id = "search_agents_endpoint" )
104+ async def search_agents (
105+ query : str = Query (..., description = "Search query" ),
106+ limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results" )
107+ ):
108+ """Search for agents by name, display_name, or content"""
109+ results = search_service .search_agents (query , limit )
110+ return {"results" : results }
111+
112+ @router .get ("/search/rules" , tags = ["mcp" ], operation_id = "search_rules_endpoint" )
113+ async def search_rules (
114+ query : str = Query (..., description = "Search query" ),
115+ limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results" )
116+ ):
117+ """Search for rules by name, display_name, content, tags, or author"""
118+ results = search_service .search_rules (query , limit )
119+ return {"results" : results }
120+
121+ @router .get ("/search/mcps" , tags = ["mcp" ], operation_id = "search_mcps_endpoint" )
122+ async def search_mcps (
123+ query : str = Query (..., description = "Search query" ),
124+ limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results" )
125+ ):
126+ """Search for MCPs by name or config content"""
127+ results = search_service .search_mcps (query , limit )
128+ return {"results" : results }
129+
130+ @router .get ("/search" , tags = ["mcp" ], operation_id = "search_all_endpoint" )
131+ async def search_all (
132+ query : str = Query (..., description = "Search query" ),
133+ limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results per category" )
134+ ):
135+ """Search across all types (agents, rules, MCPs)"""
136+ return search_service .search_all (query , limit )
137+
138+ @router .get ("/rules/{rule_ids}" , tags = ["mcp" ], operation_id = "get_multiple_rules_content" )
139+ async def get_multiple_rules_content (rule_ids : str ):
140+ """Get content for multiple rules by comma-separated IDs/slugs"""
141+ ids = [id .strip () for id in rule_ids .split (',' ) if id .strip ()]
142+
143+ if not ids :
144+ raise HTTPException (status_code = 400 , detail = "No rule IDs provided" )
145+
146+ rules = actions_loader .get_rules ()
147+ results = []
148+
149+ for rule_id in ids :
150+ # Match by slug first, fallback to name for backward compat
151+ rule = next ((r for r in rules if (r .slug == rule_id or r .name == rule_id )), None )
152+
153+ if rule :
154+ results .append ({
155+ "id" : rule_id ,
156+ "slug" : rule .slug ,
157+ "name" : rule .name ,
158+ "display_name" : rule .display_name ,
159+ "content" : rule .content ,
160+ "filename" : rule .filename
161+ })
162+ else :
163+ results .append ({
164+ "id" : rule_id ,
165+ "error" : f"Rule not found: { rule_id } "
166+ })
167+
168+ return {"rules" : results }
169+
170+ @router .get ("/agents/{agent_ids}" , tags = ["mcp" ], operation_id = "get_multiple_agents_content" )
171+ async def get_multiple_agents_content (agent_ids : str ):
172+ """Get content for multiple agents by comma-separated IDs/slugs"""
173+ ids = [id .strip () for id in agent_ids .split (',' ) if id .strip ()]
174+
175+ if not ids :
176+ raise HTTPException (status_code = 400 , detail = "No agent IDs provided" )
177+
178+ agents = actions_loader .get_agents ()
179+ results = []
180+
181+ for agent_id in ids :
182+ # Match by slug first, fallback to name for backward compat
183+ agent = next ((a for a in agents if (a .slug == agent_id or a .name == agent_id )), None )
184+
185+ if agent :
186+ results .append ({
187+ "id" : agent_id ,
188+ "slug" : agent .slug ,
189+ "name" : agent .name ,
190+ "display_name" : agent .display_name ,
191+ "content" : agent .content ,
192+ "filename" : agent .filename
193+ })
194+ else :
195+ results .append ({
196+ "id" : agent_id ,
197+ "error" : f"Agent not found: { agent_id } "
198+ })
199+
200+ return {"agents" : results }
201+
202+ @router .get ("/mcps/{mcp_ids}" , tags = ["mcp" ], operation_id = "get_multiple_mcps_config" )
203+ async def get_multiple_mcps_config (mcp_ids : str ):
204+ """Get config for multiple MCPs by comma-separated names"""
205+ ids = [id .strip () for id in mcp_ids .split (',' ) if id .strip ()]
206+
207+ if not ids :
208+ raise HTTPException (status_code = 400 , detail = "No MCP IDs provided" )
209+
210+ mcps = actions_loader .get_mcps ()
211+ results = []
212+
213+ for mcp_id in ids :
214+ # Match by name
215+ mcp = next ((m for m in mcps if m .name == mcp_id ), None )
216+
217+ if mcp :
218+ results .append ({
219+ "id" : mcp_id ,
220+ "name" : mcp .name ,
221+ "config" : mcp .config
222+ })
223+ else :
224+ results .append ({
225+ "id" : mcp_id ,
226+ "error" : f"MCP not found: { mcp_id } "
227+ })
228+
229+ return {"mcps" : results }
0 commit comments