@@ -128,3 +128,327 @@ func MakeToolHandler(f *cmdutil.Factory, actionType string) mcp.ToolHandler {
128128 }, nil
129129 }
130130}
131+
132+ // getStringArg extracts a string value from the args map.
133+ func getStringArg (args map [string ]any , key string ) string {
134+ if v , ok := args [key ]; ok {
135+ if s , ok := v .(string ); ok {
136+ return s
137+ }
138+ }
139+ return ""
140+ }
141+
142+ // makeStaticHandler creates a handler for a static tool that calls the
143+ // KeeperHub API directly with a configurable HTTP method and URL pattern.
144+ func makeStaticHandler (
145+ f * cmdutil.Factory ,
146+ method string ,
147+ buildURL func (args map [string ]any , baseURL string ) string ,
148+ buildBody func (args map [string ]any ) ([]byte , error ),
149+ ) mcp.ToolHandler {
150+ return func (ctx context.Context , req * mcp.CallToolRequest ) (* mcp.CallToolResult , error ) {
151+ var args map [string ]any
152+ if req .Params .Arguments != nil {
153+ if unmarshalErr := json .Unmarshal (req .Params .Arguments , & args ); unmarshalErr != nil {
154+ return nil , fmt .Errorf ("unmarshaling arguments: %w" , unmarshalErr )
155+ }
156+ }
157+ if args == nil {
158+ args = make (map [string ]any )
159+ }
160+
161+ client , err := f .HTTPClient ()
162+ if err != nil {
163+ return nil , fmt .Errorf ("creating HTTP client: %w" , err )
164+ }
165+
166+ cfg , err := f .Config ()
167+ if err != nil {
168+ return nil , fmt .Errorf ("reading config: %w" , err )
169+ }
170+
171+ baseURL := khhttp .BuildBaseURL (cmdutil .ResolveHost (nil , cfg ))
172+ targetURL := buildURL (args , baseURL )
173+
174+ var body io.Reader
175+ if buildBody != nil {
176+ bodyBytes , bodyErr := buildBody (args )
177+ if bodyErr != nil {
178+ return nil , fmt .Errorf ("building request body: %w" , bodyErr )
179+ }
180+ body = bytes .NewReader (bodyBytes )
181+ }
182+
183+ httpReq , err := client .NewRequest (method , targetURL , body )
184+ if err != nil {
185+ return nil , fmt .Errorf ("building request: %w" , err )
186+ }
187+ if body != nil {
188+ httpReq .Header .Set ("Content-Type" , "application/json" )
189+ }
190+
191+ resp , err := client .Do (httpReq )
192+ if err != nil {
193+ return nil , fmt .Errorf ("executing request: %w" , err )
194+ }
195+ defer resp .Body .Close ()
196+
197+ respBody , err := io .ReadAll (resp .Body )
198+ if err != nil {
199+ return nil , fmt .Errorf ("reading response body: %w" , err )
200+ }
201+
202+ if resp .StatusCode >= 400 {
203+ return & mcp.CallToolResult {
204+ Content : []mcp.Content {
205+ & mcp.TextContent {Text : fmt .Sprintf ("HTTP %d: %s" , resp .StatusCode , string (respBody ))},
206+ },
207+ IsError : true ,
208+ }, nil
209+ }
210+
211+ return & mcp.CallToolResult {
212+ Content : []mcp.Content {
213+ & mcp.TextContent {Text : string (respBody )},
214+ },
215+ }, nil
216+ }
217+ }
218+
219+ // registerStaticTools registers workflow management and execution tools that
220+ // call KeeperHub API endpoints directly (not via /api/execute/).
221+ func registerStaticTools (server * mcp.Server , f * cmdutil.Factory ) {
222+ // workflow_list -- GET /api/workflows
223+ server .AddTool (& mcp.Tool {
224+ Name : "workflow_list" ,
225+ Description : "List all workflows in the current organization" ,
226+ InputSchema : map [string ]any {
227+ "type" : "object" ,
228+ "properties" : map [string ]any {
229+ "limit" : map [string ]any {
230+ "type" : "string" ,
231+ "description" : "Maximum number of workflows to return" ,
232+ },
233+ },
234+ },
235+ }, makeStaticHandler (f , http .MethodGet , func (args map [string ]any , baseURL string ) string {
236+ u := baseURL + "/api/workflows"
237+ if limit := getStringArg (args , "limit" ); limit != "" {
238+ u += "?limit=" + limit
239+ }
240+ return u
241+ }, nil ))
242+
243+ // workflow_get -- GET /api/workflows/{id}
244+ server .AddTool (& mcp.Tool {
245+ Name : "workflow_get" ,
246+ Description : "Get a workflow by ID, including its nodes and edges" ,
247+ InputSchema : map [string ]any {
248+ "type" : "object" ,
249+ "properties" : map [string ]any {
250+ "workflow_id" : map [string ]any {
251+ "type" : "string" ,
252+ "description" : "The workflow ID" ,
253+ },
254+ },
255+ "required" : []string {"workflow_id" },
256+ },
257+ }, makeStaticHandler (f , http .MethodGet , func (args map [string ]any , baseURL string ) string {
258+ return baseURL + "/api/workflows/" + getStringArg (args , "workflow_id" )
259+ }, nil ))
260+
261+ // workflow_create -- POST /api/workflows/create
262+ server .AddTool (& mcp.Tool {
263+ Name : "workflow_create" ,
264+ Description : "Create a new workflow" ,
265+ InputSchema : map [string ]any {
266+ "type" : "object" ,
267+ "properties" : map [string ]any {
268+ "name" : map [string ]any {
269+ "type" : "string" ,
270+ "description" : "Workflow name" ,
271+ },
272+ "description" : map [string ]any {
273+ "type" : "string" ,
274+ "description" : "Workflow description" ,
275+ },
276+ "nodes" : map [string ]any {
277+ "type" : "string" ,
278+ "description" : "JSON string of the nodes array" ,
279+ },
280+ "edges" : map [string ]any {
281+ "type" : "string" ,
282+ "description" : "JSON string of the edges array" ,
283+ },
284+ },
285+ "required" : []string {"name" },
286+ },
287+ }, makeStaticHandler (f , http .MethodPost , func (args map [string ]any , baseURL string ) string {
288+ return baseURL + "/api/workflows/create"
289+ }, func (args map [string ]any ) ([]byte , error ) {
290+ body := map [string ]any {
291+ "name" : getStringArg (args , "name" ),
292+ }
293+ if desc := getStringArg (args , "description" ); desc != "" {
294+ body ["description" ] = desc
295+ }
296+ if nodesStr := getStringArg (args , "nodes" ); nodesStr != "" {
297+ var nodes []interface {}
298+ if err := json .Unmarshal ([]byte (nodesStr ), & nodes ); err != nil {
299+ return nil , fmt .Errorf ("parsing nodes JSON: %w" , err )
300+ }
301+ body ["nodes" ] = nodes
302+ }
303+ if edgesStr := getStringArg (args , "edges" ); edgesStr != "" {
304+ var edges []interface {}
305+ if err := json .Unmarshal ([]byte (edgesStr ), & edges ); err != nil {
306+ return nil , fmt .Errorf ("parsing edges JSON: %w" , err )
307+ }
308+ body ["edges" ] = edges
309+ }
310+ return json .Marshal (body )
311+ }))
312+
313+ // workflow_update -- PATCH /api/workflows/{id}
314+ server .AddTool (& mcp.Tool {
315+ Name : "workflow_update" ,
316+ Description : "Update an existing workflow" ,
317+ InputSchema : map [string ]any {
318+ "type" : "object" ,
319+ "properties" : map [string ]any {
320+ "workflow_id" : map [string ]any {
321+ "type" : "string" ,
322+ "description" : "The workflow ID to update" ,
323+ },
324+ "name" : map [string ]any {
325+ "type" : "string" ,
326+ "description" : "New workflow name" ,
327+ },
328+ "description" : map [string ]any {
329+ "type" : "string" ,
330+ "description" : "New workflow description" ,
331+ },
332+ "nodes" : map [string ]any {
333+ "type" : "string" ,
334+ "description" : "JSON string of the nodes array" ,
335+ },
336+ "edges" : map [string ]any {
337+ "type" : "string" ,
338+ "description" : "JSON string of the edges array" ,
339+ },
340+ },
341+ "required" : []string {"workflow_id" },
342+ },
343+ }, makeStaticHandler (f , http .MethodPatch , func (args map [string ]any , baseURL string ) string {
344+ return baseURL + "/api/workflows/" + getStringArg (args , "workflow_id" )
345+ }, func (args map [string ]any ) ([]byte , error ) {
346+ body := make (map [string ]any )
347+ if name := getStringArg (args , "name" ); name != "" {
348+ body ["name" ] = name
349+ }
350+ if desc := getStringArg (args , "description" ); desc != "" {
351+ body ["description" ] = desc
352+ }
353+ if nodesStr := getStringArg (args , "nodes" ); nodesStr != "" {
354+ var nodes []interface {}
355+ if err := json .Unmarshal ([]byte (nodesStr ), & nodes ); err != nil {
356+ return nil , fmt .Errorf ("parsing nodes JSON: %w" , err )
357+ }
358+ body ["nodes" ] = nodes
359+ }
360+ if edgesStr := getStringArg (args , "edges" ); edgesStr != "" {
361+ var edges []interface {}
362+ if err := json .Unmarshal ([]byte (edgesStr ), & edges ); err != nil {
363+ return nil , fmt .Errorf ("parsing edges JSON: %w" , err )
364+ }
365+ body ["edges" ] = edges
366+ }
367+ return json .Marshal (body )
368+ }))
369+
370+ // workflow_delete -- DELETE /api/workflows/{id}
371+ server .AddTool (& mcp.Tool {
372+ Name : "workflow_delete" ,
373+ Description : "Delete a workflow by ID" ,
374+ InputSchema : map [string ]any {
375+ "type" : "object" ,
376+ "properties" : map [string ]any {
377+ "workflow_id" : map [string ]any {
378+ "type" : "string" ,
379+ "description" : "The workflow ID to delete" ,
380+ },
381+ },
382+ "required" : []string {"workflow_id" },
383+ },
384+ }, makeStaticHandler (f , http .MethodDelete , func (args map [string ]any , baseURL string ) string {
385+ return baseURL + "/api/workflows/" + getStringArg (args , "workflow_id" )
386+ }, nil ))
387+
388+ // workflow_execute -- POST /api/workflow/{id}/execute
389+ server .AddTool (& mcp.Tool {
390+ Name : "workflow_execute" ,
391+ Description : "Execute a workflow by ID" ,
392+ InputSchema : map [string ]any {
393+ "type" : "object" ,
394+ "properties" : map [string ]any {
395+ "workflow_id" : map [string ]any {
396+ "type" : "string" ,
397+ "description" : "The workflow ID to execute" ,
398+ },
399+ "input" : map [string ]any {
400+ "type" : "string" ,
401+ "description" : "JSON string of input data for the execution" ,
402+ },
403+ },
404+ "required" : []string {"workflow_id" },
405+ },
406+ }, makeStaticHandler (f , http .MethodPost , func (args map [string ]any , baseURL string ) string {
407+ return baseURL + "/api/workflow/" + getStringArg (args , "workflow_id" ) + "/execute"
408+ }, func (args map [string ]any ) ([]byte , error ) {
409+ if inputStr := getStringArg (args , "input" ); inputStr != "" {
410+ var input map [string ]interface {}
411+ if err := json .Unmarshal ([]byte (inputStr ), & input ); err != nil {
412+ return nil , fmt .Errorf ("parsing input JSON: %w" , err )
413+ }
414+ return json .Marshal (input )
415+ }
416+ return []byte ("{}" ), nil
417+ }))
418+
419+ // execution_status -- GET /api/workflows/executions/{id}/status
420+ server .AddTool (& mcp.Tool {
421+ Name : "execution_status" ,
422+ Description : "Get the status of a workflow execution" ,
423+ InputSchema : map [string ]any {
424+ "type" : "object" ,
425+ "properties" : map [string ]any {
426+ "execution_id" : map [string ]any {
427+ "type" : "string" ,
428+ "description" : "The execution ID to check" ,
429+ },
430+ },
431+ "required" : []string {"execution_id" },
432+ },
433+ }, makeStaticHandler (f , http .MethodGet , func (args map [string ]any , baseURL string ) string {
434+ return baseURL + "/api/workflows/executions/" + getStringArg (args , "execution_id" ) + "/status"
435+ }, nil ))
436+
437+ // execution_logs -- GET /api/workflows/executions/{id}/logs
438+ server .AddTool (& mcp.Tool {
439+ Name : "execution_logs" ,
440+ Description : "Get the logs for a workflow execution" ,
441+ InputSchema : map [string ]any {
442+ "type" : "object" ,
443+ "properties" : map [string ]any {
444+ "execution_id" : map [string ]any {
445+ "type" : "string" ,
446+ "description" : "The execution ID to get logs for" ,
447+ },
448+ },
449+ "required" : []string {"execution_id" },
450+ },
451+ }, makeStaticHandler (f , http .MethodGet , func (args map [string ]any , baseURL string ) string {
452+ return baseURL + "/api/workflows/executions/" + getStringArg (args , "execution_id" ) + "/logs"
453+ }, nil ))
454+ }
0 commit comments