Skip to content

Commit e7920b3

Browse files
harjas27Harness
authored andcommitted
[feat]:[IDP-6019]: Tools for workflow creation and execution (#116)
* address review comments * use pipeline yaml in create workflow * e2e tests * improve description * [feat]:[IDP-6019]: Tools for workflow creation and execution
1 parent f499b62 commit e7920b3

File tree

8 files changed

+441
-19
lines changed

8 files changed

+441
-19
lines changed

client/dto/genai.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ func (d *DBChangesetParameters) IsStreaming() bool {
120120
return d.BaseRequestParameters.Stream
121121
}
122122

123+
// IDPWorkflowParameters extends BaseRequestParameters for workflow generation
124+
type IDPWorkflowParameters struct {
125+
BaseRequestParameters
126+
PipelineInfo string `json:"pipeline_info"`
127+
OldWorkflow string `json:"oldworkflow,omitempty"`
128+
ErrorContext string `json:"error_context,omitempty"`
129+
}
130+
123131
type CapabilityToRun struct {
124132
CallID string `json:"call_id"`
125133
Type string `json:"type"`

client/dto/idp.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,12 @@ type ScorecardScore struct {
217217
Score int `json:"score"`
218218
ScorecardName string `json:"scorecard_name"`
219219
}
220+
221+
type ExecuteWorkflowRequest struct {
222+
Identifier string `json:"identifier"`
223+
Values interface{} `json:"values,omitempty"`
224+
}
225+
226+
type ExecuteWorkflowResponse struct {
227+
ExecutionID string `json:"id"`
228+
}

client/genai.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
const (
1717
aiDevopsChatPath = "chat/platform"
1818
dbChangesetPath = "chat/db-changeset"
19+
idpWorkflowPath = "chat/idp-workflow"
1920
)
2021

2122
type GenaiService struct {
@@ -112,6 +113,12 @@ func (g *GenaiService) SendDBChangeset(ctx context.Context, scope dto.Scope, req
112113
return g.sendGenAIRequest(ctx, dbChangesetPath, scope, request, onProgress...)
113114
}
114115

116+
// SendIDPWorkflow sends a request to generate idp workflows and returns the response.
117+
// If request.Stream is true and onProgress is provided, it will handle streaming responses.
118+
func (g *GenaiService) SendIDPWorkflow(ctx context.Context, scope dto.Scope, request *dto.IDPWorkflowParameters, onProgress ...func(progressUpdate dto.ProgressUpdate) error) (*dto.ServiceChatResponse, error) {
119+
return g.sendGenAIRequest(ctx, idpWorkflowPath, scope, request, onProgress...)
120+
}
121+
115122
// processStreamingResponse handles Server-Sent Events (SSE) streaming responses
116123
// and accumulates complete events before forwarding them with appropriate event types
117124
func (g *GenaiService) processStreamingResponse(body io.ReadCloser, finalResponse *dto.ServiceChatResponse, onProgress func(dto.ProgressUpdate) error) error {

client/idp.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
idpListScorecardsPath = "/v1/scorecards"
1717
idpGetScoreSummaryPath = "/v1/scores/summary"
1818
idpGetScoresPath = "/v1/scores"
19+
idpExecuteWorkflowPath = "/v2/workflows/execute"
1920

2021
// Default values for requests
2122
defaultKind = "component,api,resource"
@@ -45,7 +46,7 @@ func (i *IDPService) GetEntity(ctx context.Context, scope dto.Scope, kind string
4546

4647
response := new(dto.EntityResponse)
4748

48-
err := i.Client.Get(ctx, path, params, headers, response)
49+
err := i.Client.Get(ctx, path, params, map[string]string{}, response)
4950
if err != nil {
5051
return nil, err
5152
}
@@ -78,9 +79,7 @@ func (i *IDPService) ListEntities(ctx context.Context, scope dto.Scope, getEntit
7879
params["search_term"] = getEntitiesParams.SearchTerm
7980
}
8081

81-
if getEntitiesParams.Scopes != "" {
82-
params["scopes"] = getEntitiesParams.Scopes
83-
}
82+
params["scopes"] = generateScopeParamVal(scope)
8483

8584
params["owned_by_me"] = fmt.Sprintf("%v", getEntitiesParams.OwnedByMe)
8685
params["favorites"] = fmt.Sprintf("%v", getEntitiesParams.Favorites)
@@ -109,7 +108,7 @@ func (i *IDPService) ListEntities(ctx context.Context, scope dto.Scope, getEntit
109108

110109
response := make([]dto.EntityResponse, 0)
111110

112-
err := i.Client.Get(ctx, path, params, headers, &response)
111+
err := i.Client.Get(ctx, path, params, map[string]string{}, &response)
113112
if err != nil {
114113
return nil, err
115114
}
@@ -140,7 +139,6 @@ func (i *IDPService) ListScorecards(ctx context.Context, scope dto.Scope) (*[]dt
140139
addHarnessAccountToHeaders(scope, headers)
141140

142141
response := make([]dto.ScorecardResponse, 0)
143-
144142
err := i.Client.Get(ctx, path, map[string]string{}, headers, &response)
145143
if err != nil {
146144
return nil, err
@@ -159,7 +157,6 @@ func (i *IDPService) GetScorecardSummary(ctx context.Context, scope dto.Scope, i
159157
params["entity_identifier"] = identifier
160158

161159
response := new(dto.ScorecardSummaryResponse)
162-
163160
err := i.Client.Get(ctx, path, params, headers, response)
164161
if err != nil {
165162
return nil, err
@@ -178,7 +175,6 @@ func (i *IDPService) GetScorecardScores(ctx context.Context, scope dto.Scope, id
178175
params["entity_identifier"] = identifier
179176

180177
response := new(dto.ScorecardScoreResponse)
181-
182178
err := i.Client.Get(ctx, path, params, headers, response)
183179
if err != nil {
184180
return nil, err
@@ -187,6 +183,36 @@ func (i *IDPService) GetScorecardScores(ctx context.Context, scope dto.Scope, id
187183
return response, nil
188184
}
189185

186+
func (i *IDPService) ExecuteWorkflow(ctx context.Context, scope dto.Scope, identifier string, inputSet map[string]interface{}) (*dto.ExecuteWorkflowResponse, error) {
187+
path := idpExecuteWorkflowPath
188+
189+
headers := make(map[string]string)
190+
addHarnessAccountToHeaders(scope, headers)
191+
192+
params := make(map[string]string)
193+
addScope(scope, params)
194+
195+
_, token, err := i.Client.AuthProvider.GetHeader(ctx)
196+
if err != nil {
197+
slog.Error("Failed to get auth header", "error", err)
198+
return nil, err
199+
}
200+
inputSet["token"] = token
201+
body := new(dto.ExecuteWorkflowRequest)
202+
body.Identifier = identifier
203+
body.Values = inputSet
204+
205+
response := new(dto.ExecuteWorkflowResponse)
206+
207+
err = i.Client.Post(ctx, path, params, body, response)
208+
if err != nil {
209+
slog.Error("Failed to execute workflow", "error", err)
210+
return nil, err
211+
}
212+
213+
return response, nil
214+
}
215+
190216
func generateScopeParamVal(scope dto.Scope) string {
191217
scopeParam := defaultScope
192218
if scope.AccountID != "" {

pkg/harness/tools/genai.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ func createGenAIToolHandler(config *config.Config, client *client.GenaiService,
210210
case *dto.DBChangesetParameters:
211211
req.Stream = shouldStream
212212
response, respErr = client.SendDBChangeset(ctx, scope, req, onProgress)
213+
case *dto.IDPWorkflowParameters:
214+
req.Stream = shouldStream
215+
response, respErr = client.SendIDPWorkflow(ctx, scope, req, onProgress)
213216
default:
214217
return nil, fmt.Errorf("unsupported request type: %T", requestObj)
215218
}
@@ -342,3 +345,72 @@ func DBChangesetTool(config *config.Config, client *client.GenaiService) (tool m
342345
})
343346
return tool, handler
344347
}
348+
349+
// GenerateWorflowTool creates a tool for generating idp workflows
350+
func GenerateWorflowTool(config *config.Config, client *client.GenaiService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
351+
// Get common parameters
352+
commonParams := getCommonGenAIParameters()
353+
354+
// Add tool-specific parameters
355+
toolParams := append(commonParams,
356+
mcp.WithString("pipeline_info",
357+
mcp.Required(),
358+
mcp.Description("The YAML of the pipeline which is to be used for generating the workflow. Use the get_pipeline tool to retrieve the pipeline YAML."),
359+
),
360+
mcp.WithString("old_workflow",
361+
mcp.Description("Optional existing workflow YAML for updates"),
362+
),
363+
mcp.WithString("error_context",
364+
mcp.Description("Optional error context if this is a retry after an error for a given workflow"),
365+
),
366+
WithScope(config, false),
367+
)
368+
369+
tool = mcp.NewTool("generate_idp_workflow",
370+
append([]mcp.ToolOption{mcp.WithDescription(`
371+
Generates the YAML for an IDP Workflow.
372+
Usage Guidance:
373+
- Use this tool to generate a workflow YAML using a given pipeline.
374+
- You must provide the pipline YAML to generate the workflow YAML.
375+
`)},
376+
toolParams...)...,
377+
)
378+
379+
handler = createGenAIToolHandler(config, client, func(baseParams *dto.BaseRequestParameters, request mcp.CallToolRequest) (interface{}, error) {
380+
381+
pipelineInfoArg, ok := request.GetArguments()["pipeline_info"]
382+
if !ok || pipelineInfoArg == nil {
383+
return nil, fmt.Errorf("missing required parameter: pipeline_info")
384+
}
385+
386+
pipelineInfo, ok := pipelineInfoArg.(string)
387+
if !ok {
388+
return nil, fmt.Errorf("pipeline_info must be a string")
389+
}
390+
391+
oldWorkflowArg, ok := request.GetArguments()["old_workflow"]
392+
var oldWorkflow string
393+
if ok && oldWorkflowArg != nil {
394+
if oc, isString := oldWorkflowArg.(string); isString {
395+
oldWorkflow = oc
396+
}
397+
}
398+
399+
errorContextArg, ok := request.GetArguments()["error_context"]
400+
var errorContext string
401+
if ok && errorContextArg != nil {
402+
if ec, isString := errorContextArg.(string); isString {
403+
errorContext = ec
404+
}
405+
}
406+
407+
// Create the IDP Workflow parameters
408+
return &dto.IDPWorkflowParameters{
409+
BaseRequestParameters: *baseParams,
410+
PipelineInfo: pipelineInfo,
411+
OldWorkflow: oldWorkflow,
412+
ErrorContext: errorContext,
413+
}, nil
414+
})
415+
return tool, handler
416+
}

pkg/harness/tools/idp.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
78
"github.com/harness/harness-mcp/client"
89
"github.com/harness/harness-mcp/client/dto"
910
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
@@ -13,7 +14,8 @@ import (
1314

1415
func GetEntityTool(config *config.Config, client *client.IDPService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1516
return mcp.NewTool("get_entity",
16-
mcp.WithDescription("Get details of a specific entity(services, APIs, user groups, resources) in the Harness IDP Catalog. Entities can represent services, APIs, user groups, resources, and more. The tool returns metadata for the Harness entities matching the filter criteria, including their identifier, scope, kind, reference type (INLINE/GIT), YAML definition, Git details (branch, path, repo), ownership, tags, lifecycle, scorecards, status, and group. Use the list_entities tool to first to get the id."),
17+
mcp.WithDescription(`Get details of a specific entity(services, APIs, user groups, resources) in the Harness IDP Catalog. Entities can represent services, APIs, user groups, resources, and more. The tool returns metadata for the Harness entities matching the filter criteria, including their identifier, scope, kind, reference type (INLINE/GIT), YAML definition, Git details (branch, path, repo), ownership, tags, lifecycle, scorecards, status, and group. Use the list_entities tool to first to get the id.
18+
Note: If the fetched entity is a workflow, it might contain a token field but that is to be IGNORED.`),
1719
WithScope(config, false),
1820
mcp.WithString("entity_id",
1921
mcp.Required(),
@@ -54,7 +56,8 @@ func GetEntityTool(config *config.Config, client *client.IDPService) (tool mcp.T
5456

5557
func ListEntitiesTool(config *config.Config, client *client.IDPService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
5658
return mcp.NewTool("list_entities",
57-
mcp.WithDescription("List entities in the Harness Internal Developer Portal Catalog. Entities can represent services, APIs, user groups, resources, and more. The tool returns metadata for the Harness entities matching the filter criteria, including their identifier, scope, kind, reference type (INLINE/GIT), YAML definition, Git details (branch, path, repo), ownership, tags, lifecycle, scorecards, status, and group."),
59+
mcp.WithDescription(`List entities in the Harness Internal Developer Portal Catalog. Entities can represent services, APIs, user groups, resources, and more. The tool returns metadata for the Harness entities matching the filter criteria, including their identifier, scope, kind, reference type (INLINE/GIT), YAML definition, Git details (branch, path, repo), ownership, tags, lifecycle, scorecards, status, and group.
60+
Note: If the fetched entity is a workflow, it might contain a token field but that is to be IGNORED.`),
5861
mcp.WithString("search_term",
5962
mcp.Description("Optional search term to filter entities"),
6063
),
@@ -290,3 +293,60 @@ func GetScoresTool(config *config.Config, client *client.IDPService) (tool mcp.T
290293
return mcp.NewToolResultText(string(r)), nil
291294
}
292295
}
296+
297+
func ExecuteWorkflowTool(config *config.Config, client *client.IDPService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
298+
return mcp.NewTool("execute_workflow",
299+
mcp.WithDescription(`Execute a workflow in the Harness Internal Developer Portal Catalog. This tool takes in the entity metadata of the workflow and a set of values to be used for the execution.
300+
Usage Guidance:
301+
- Use the get_entity tool to fetch the workflow details
302+
- The set of values provided has to be validated against the input set required by the workflow.
303+
- Provide only non-authentication parameters in the values object
304+
- All HarnessAuthToken fields should be OMITTED regardless of workflow requirements
305+
- Validate other required parameters against the workflow's input set
306+
⚠️ IMPORTANT:
307+
- NEVER request or include token values when executing workflows. The system handles authentication automatically - DO NOT prompt users for tokens, even if they appear as required parameters in the workflow definition.
308+
- DO NOT execute the workflow if the valueset is not sufficient.`),
309+
WithScope(config, false),
310+
mcp.WithObject("workflow_details",
311+
mcp.Required(),
312+
mcp.Description("A json representation of the workflow entity. This json contains the metadata of the workflow(like owner, name, description, ref etc) and a yaml field which should contain the spec.parameters against which the values should be validated. Only the parameters marked required are mandatory."),
313+
),
314+
mcp.WithObject("identifier",
315+
mcp.Required(),
316+
mcp.Description("The identifier of the workflow to be executed. This can be extracted from the field identifier of the workflow_details."),
317+
),
318+
mcp.WithObject("values",
319+
mcp.Description("The values to be used for the workflow execution. The values should be in the format of a json object. These values are to be validated against the parameters of the workflow. Do NOT validate the field of type HarnessAuthToken, it is not to be provided in the prompt."),
320+
),
321+
),
322+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
323+
scope, err := FetchScope(config, request, false)
324+
if err != nil {
325+
return mcp.NewToolResultError(err.Error()), nil
326+
}
327+
_, err = RequiredParam[interface{}](request, "workflow_details")
328+
if err != nil {
329+
return mcp.NewToolResultError(err.Error()), nil
330+
}
331+
identifier, err := RequiredParam[string](request, "identifier")
332+
if err != nil {
333+
return mcp.NewToolResultError(err.Error()), nil
334+
}
335+
values, err := OptionalParam[map[string]interface{}](request, "values")
336+
if err != nil {
337+
return mcp.NewToolResultError(err.Error()), nil
338+
}
339+
340+
data, err := client.ExecuteWorkflow(ctx, scope, identifier, values)
341+
if err != nil {
342+
return nil, fmt.Errorf("failed to execute workflow: %w", err)
343+
}
344+
345+
r, err := json.Marshal(data)
346+
if err != nil {
347+
return nil, fmt.Errorf("failed to marshal workflow execution response: %w", err)
348+
}
349+
350+
return mcp.NewToolResultText(string(r)), nil
351+
}
352+
}

pkg/modules/idp.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package modules
22

33
import (
4+
"fmt"
5+
46
"github.com/harness/harness-mcp/client"
57
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
68
"github.com/harness/harness-mcp/pkg/harness/tools"
79
"github.com/harness/harness-mcp/pkg/modules/utils"
810
"github.com/harness/harness-mcp/pkg/toolsets"
11+
"github.com/mark3labs/mcp-go/server"
912
)
1013

1114
// IDPModule implements the Module interface for Internal Developer Portal
@@ -76,15 +79,29 @@ func RegisterInternalDeveloperPortal(config *config.Config, tsg *toolsets.Toolse
7679
Client: c,
7780
}
7881

79-
idp := toolsets.NewToolset("Internal Developer Portal", "Harness Internal Developer Portal catalog related tools for managing catalog Entities which represent the core components of your system").
80-
AddReadTools(
81-
toolsets.NewServerTool(tools.ListEntitiesTool(config, idpClient)),
82-
toolsets.NewServerTool(tools.GetEntityTool(config, idpClient)),
83-
toolsets.NewServerTool(tools.GetScorecardTool(config, idpClient)),
84-
toolsets.NewServerTool(tools.ListScorecardsTool(config, idpClient)),
85-
toolsets.NewServerTool(tools.GetScoreSummaryTool(config, idpClient)),
86-
toolsets.NewServerTool(tools.GetScoresTool(config, idpClient)),
87-
)
82+
// Get the GenAI client using the shared method
83+
genaiClient, err := GetGenAIClient(config)
84+
if err != nil {
85+
return fmt.Errorf("failed to create client for genai: %w", err)
86+
}
87+
88+
idpTools := []server.ServerTool{
89+
toolsets.NewServerTool(tools.ListEntitiesTool(config, idpClient)),
90+
toolsets.NewServerTool(tools.GetEntityTool(config, idpClient)),
91+
toolsets.NewServerTool(tools.GetScorecardTool(config, idpClient)),
92+
toolsets.NewServerTool(tools.ListScorecardsTool(config, idpClient)),
93+
toolsets.NewServerTool(tools.GetScoreSummaryTool(config, idpClient)),
94+
toolsets.NewServerTool(tools.GetScoresTool(config, idpClient)),
95+
toolsets.NewServerTool(tools.ExecuteWorkflowTool(config, idpClient)),
96+
}
97+
98+
// Add GenerateWorflowTool only if genaiClient is available
99+
if genaiClient != nil {
100+
idpTools = append(idpTools, toolsets.NewServerTool(tools.GenerateWorflowTool(config, genaiClient)))
101+
}
102+
103+
idp := toolsets.NewToolset("idp", "Harness Internal Developer Portal catalog related tools for managing catalog Entities which represent the core components of your system").
104+
AddReadTools(idpTools...)
88105

89106
// Add toolset to the group
90107
tsg.AddToolset(idp)

0 commit comments

Comments
 (0)