1- from fastapi import APIRouter , HTTPException , Body , Query
2- from app .models .actions import ActionsResponse , Agent , Rule , MCP , Action , ActionType , ActionsListResponse
1+ from fastapi import APIRouter , HTTPException , Query
2+ from app .models .actions import Action , ActionType , ActionsListResponse
33from app .services .actions_loader import actions_loader
4- from app .services .mcp_installer import get_agent_content , get_rule_content , create_mcp_config
5- from app .services .search_service import search_service
6- from typing import List , Dict , Any , Optional
7- import json
4+ from typing import Optional
85
96router = APIRouter (prefix = "/api" , tags = ["actions" ])
107
11- @router .get ("/v2/ actions" , response_model = ActionsListResponse , operation_id = "get_unified_actions" )
8+ @router .get ("/actions" , response_model = ActionsListResponse , operation_id = "get_unified_actions" )
129async def get_unified_actions (
1310 action_type : Optional [ActionType ] = Query (None , description = "Filter by action type" ),
1411 tags : Optional [str ] = Query (None , description = "Comma-separated list of tags to filter by" ),
@@ -44,275 +41,7 @@ async def get_unified_actions(
4441 has_more = (offset + limit ) < total
4542 )
4643
47- @router .get ("/v2/actions/{action_id}" , response_model = Action , operation_id = "get_action_by_id" )
48- async def get_action_by_id (action_id : str ):
49- """Get a specific action by ID"""
50- action = actions_loader .get_action_by_id (action_id )
51- if not action :
52- raise HTTPException (status_code = 404 , detail = f"Action not found: { action_id } " )
53- return action
5444
55- @router .get ("/actions" , response_model = ActionsResponse , operation_id = "get_all_actions_endpoint" )
56- async def get_all_actions ():
57- """Get all available actions (agents, rules, MCPs)"""
58- return ActionsResponse (
59- agents = actions_loader .get_agents (),
60- rules = actions_loader .get_rules (),
61- mcps = actions_loader .get_mcps ()
62- )
63-
64- @router .get ("/agents" , operation_id = "get_agents_endpoint" )
65- async def get_agents (
66- after : Optional [str ] = Query (None , description = "Cursor for pagination (use slug of last item from previous page)" ),
67- limit : int = Query (30 , ge = 1 , le = 30 , description = "Maximum number of results (max 30)" )
68- ):
69- """Get available agents with tags only, limited to 30 items"""
70- agents = actions_loader .get_agents ()
71-
72- # Find starting position if 'after' is provided
73- start_idx = 0
74- if after :
75- for idx , agent in enumerate (agents ):
76- if agent .slug == after :
77- start_idx = idx + 1
78- break
79-
80- # Apply limit
81- paginated_agents = agents [start_idx :start_idx + limit ]
82-
83- return [
84- {
85- "name" : agent .name ,
86- "display_name" : agent .display_name ,
87- "slug" : agent .slug ,
88- "tags" : agent .tags ,
89- "filename" : agent .filename
90- }
91- for agent in paginated_agents
92- ]
93-
94- @router .get ("/rules" , operation_id = "get_rules_endpoint" )
95- async def get_rules (
96- after : Optional [str ] = Query (None , description = "Cursor for pagination (use slug of last item from previous page)" ),
97- limit : int = Query (30 , ge = 1 , le = 30 , description = "Maximum number of results (max 30)" )
98- ):
99- """Get available rules with tags only, limited to 30 items"""
100- rules = actions_loader .get_rules ()
101-
102- # Find starting position if 'after' is provided
103- start_idx = 0
104- if after :
105- for idx , rule in enumerate (rules ):
106- if rule .slug == after :
107- start_idx = idx + 1
108- break
109-
110- # Apply limit
111- paginated_rules = rules [start_idx :start_idx + limit ]
112-
113- return [
114- {
115- "name" : rule .name ,
116- "display_name" : rule .display_name ,
117- "slug" : rule .slug ,
118- "tags" : rule .tags ,
119- "filename" : rule .filename
120- }
121- for rule in paginated_rules
122- ]
123-
124- @router .get ("/mcps" , operation_id = "get_mcps_endpoint" )
125- async def get_mcps (
126- after : Optional [str ] = Query (None , description = "Cursor for pagination (use name of last item from previous page)" ),
127- limit : int = Query (30 , ge = 1 , le = 30 , description = "Maximum number of results (max 30)" )
128- ):
129- """Get available MCPs with tags only, limited to 30 items"""
130- mcps = actions_loader .get_mcps ()
131-
132- # Find starting position if 'after' is provided
133- start_idx = 0
134- if after :
135- for idx , mcp in enumerate (mcps ):
136- if mcp .name == after :
137- start_idx = idx + 1
138- break
139-
140- # Apply limit
141- paginated_mcps = mcps [start_idx :start_idx + limit ]
142-
143- return [
144- {
145- "name" : mcp .name ,
146- "tags" : mcp .tags if hasattr (mcp , 'tags' ) else []
147- }
148- for mcp in paginated_mcps
149- ]
150-
151-
152-
153-
154- @router .get ("/merged-block" , operation_id = "get_merged_actions_block_endpoint" )
155- async def get_merged_actions_block ():
156- """Get all actions merged into a single block with metadata for frontend"""
157- agents = actions_loader .get_agents ()
158- rules = actions_loader .get_rules ()
159- mcps = actions_loader .get_mcps ()
160-
161- # Build merged block with all actions and their metadata
162- merged = {
163- "agents" : [
164- {
165- "display_name" : agent .display_name or agent .name ,
166- "slug" : agent .slug or agent .filename .replace ('.yaml' , '' ).replace ('.md' , '' ),
167- "content" : agent .content or get_agent_content (agent .filename ),
168- "filename" : agent .filename
169- }
170- for agent in agents
171- ],
172- "rules" : [
173- {
174- "display_name" : rule .display_name or rule .name ,
175- "slug" : rule .slug or rule .filename .replace ('.yaml' , '' ).replace ('.md' , '' ),
176- "content" : rule .content or get_rule_content (rule .filename ),
177- "filename" : rule .filename
178- }
179- for rule in rules
180- ],
181- "mcps" : [
182- {
183- "name" : mcp .name ,
184- "config" : mcp .config
185- }
186- for mcp in mcps
187- ]
188- }
189-
190- return merged
191-
192- @router .get ("/search/agents" , tags = ["mcp" ], operation_id = "search_agents_endpoint" )
193- async def search_agents (
194- query : str = Query (..., description = "Search query. Supports wildcards: * (any characters) and ? (single character)" ),
195- limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results" )
196- ):
197- """Search for agents by name, display_name, or content. Supports wildcard patterns with * and ?"""
198- results = search_service .search_agents (query , limit )
199- return {"results" : results }
20045
201- @router .get ("/search/rules" , tags = ["mcp" ], operation_id = "search_rules_endpoint" )
202- async def search_rules (
203- query : str = Query (..., description = "Search query. Supports wildcards: * (any characters) and ? (single character)" ),
204- limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results" )
205- ):
206- """Search for rules by name, display_name, content, tags, or author. Supports wildcard patterns with * and ?"""
207- results = search_service .search_rules (query , limit )
208- return {"results" : results }
20946
210- @router .get ("/search/mcps" , tags = ["mcp" ], operation_id = "search_mcps_endpoint" )
211- async def search_mcps (
212- query : str = Query (..., description = "Search query. Supports wildcards: * (any characters) and ? (single character)" ),
213- limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results" )
214- ):
215- """Search for MCPs by name or config content. Supports wildcard patterns with * and ?"""
216- results = search_service .search_mcps (query , limit )
217- return {"results" : results }
21847
219- @router .get ("/search" , tags = ["mcp" ], operation_id = "search_all_endpoint" )
220- async def search_all (
221- query : str = Query (..., description = "Search query. Supports wildcards: * (any characters) and ? (single character)" ),
222- limit : int = Query (10 , ge = 1 , le = 100 , description = "Maximum number of results per category" )
223- ):
224- """Search across all types (agents, rules, MCPs). Supports wildcard patterns with * and ?"""
225- return search_service .search_all (query , limit )
226-
227- @router .get ("/rules/{rule_ids}" , tags = ["mcp" ], operation_id = "get_multiple_rules_content" )
228- async def get_multiple_rules_content (rule_ids : str ):
229- """Get content for multiple rules by comma-separated IDs/slugs"""
230- ids = [id .strip () for id in rule_ids .split (',' ) if id .strip ()]
231-
232- if not ids :
233- raise HTTPException (status_code = 400 , detail = "No rule IDs provided" )
234-
235- rules = actions_loader .get_rules ()
236- results = []
237-
238- for rule_id in ids :
239- # Match by slug first, fallback to name for backward compat
240- rule = next ((r for r in rules if (r .slug == rule_id or r .name == rule_id )), None )
241-
242- if rule :
243- results .append ({
244- "id" : rule_id ,
245- "slug" : rule .slug ,
246- "name" : rule .name ,
247- "display_name" : rule .display_name ,
248- "content" : rule .content ,
249- "filename" : rule .filename
250- })
251- else :
252- results .append ({
253- "id" : rule_id ,
254- "error" : f"Rule not found: { rule_id } "
255- })
256-
257- return {"rules" : results }
258-
259- @router .get ("/agents/{agent_ids}" , tags = ["mcp" ], operation_id = "get_multiple_agents_content" )
260- async def get_multiple_agents_content (agent_ids : str ):
261- """Get content for multiple agents by comma-separated IDs/slugs"""
262- ids = [id .strip () for id in agent_ids .split (',' ) if id .strip ()]
263-
264- if not ids :
265- raise HTTPException (status_code = 400 , detail = "No agent IDs provided" )
266-
267- agents = actions_loader .get_agents ()
268- results = []
269-
270- for agent_id in ids :
271- # Match by slug first, fallback to name for backward compat
272- agent = next ((a for a in agents if (a .slug == agent_id or a .name == agent_id )), None )
273-
274- if agent :
275- results .append ({
276- "id" : agent_id ,
277- "slug" : agent .slug ,
278- "name" : agent .name ,
279- "display_name" : agent .display_name ,
280- "content" : agent .content ,
281- "filename" : agent .filename
282- })
283- else :
284- results .append ({
285- "id" : agent_id ,
286- "error" : f"Agent not found: { agent_id } "
287- })
288-
289- return {"agents" : results }
290-
291- @router .get ("/mcps/{mcp_ids}" , tags = ["mcp" ], operation_id = "get_multiple_mcps_config" )
292- async def get_multiple_mcps_config (mcp_ids : str ):
293- """Get config for multiple MCPs by comma-separated names. These MCPs will need to be installed on the client side (e.g. in `.mcp.json`)"""
294- ids = [id .strip () for id in mcp_ids .split (',' ) if id .strip ()]
295-
296- if not ids :
297- raise HTTPException (status_code = 400 , detail = "No MCP IDs provided" )
298-
299- mcps = actions_loader .get_mcps ()
300- results = []
301-
302- for mcp_id in ids :
303- # Match by name
304- mcp = next ((m for m in mcps if m .name == mcp_id ), None )
305-
306- if mcp :
307- results .append ({
308- "id" : mcp_id ,
309- "name" : mcp .name ,
310- "config" : mcp .config
311- })
312- else :
313- results .append ({
314- "id" : mcp_id ,
315- "error" : f"MCP not found: { mcp_id } "
316- })
317-
318- return {"mcps" : results }
0 commit comments