From 531c29b576602c5959eb6403ca1f5c2c04f6289c Mon Sep 17 00:00:00 2001 From: Utkarsh Choubey Date: Fri, 23 May 2025 17:01:15 +0530 Subject: [PATCH 1/2] feat: [PIPE-1]: Add Support for Templates Get and List Call --- .cascade-mcp-config.json | 65 ++++++++++++++++++ client/client.go | 2 + client/dto/template.go | 61 +++++++++++++++++ client/templates.go | 96 +++++++++++++++++++++++++++ pkg/harness/templates.go | 139 +++++++++++++++++++++++++++++++++++++++ pkg/harness/tools.go | 8 +++ 6 files changed, 371 insertions(+) create mode 100644 .cascade-mcp-config.json create mode 100644 client/dto/template.go create mode 100644 client/templates.go create mode 100644 pkg/harness/templates.go diff --git a/.cascade-mcp-config.json b/.cascade-mcp-config.json new file mode 100644 index 0000000..0ad79b3 --- /dev/null +++ b/.cascade-mcp-config.json @@ -0,0 +1,65 @@ +{ + "tools": [ + { + "name": "mcp0_list_templates", + "description": "List templates in Harness.", + "parameters": { + "properties": { + "org_id": { + "description": "Required ID of the organization.", + "type": "string" + }, + "project_id": { + "description": "Required ID of the project.", + "type": "string" + }, + "page": { + "description": "Page number for pagination - page 0 is the first page", + "default": 0, + "type": "number" + }, + "size": { + "description": "Number of items per page", + "default": 5, + "maximum": 20, + "type": "number" + }, + "search": { + "description": "Optional search term to filter templates", + "type": "string" + }, + "template_list_type": { + "description": "Optional type of template list (e.g., LastUpdated)", + "type": "string" + }, + "sort": { + "description": "Optional sort order (e.g., lastUpdatedAt,DESC)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "mcp0_get_template", + "description": "Get details of a specific template in Harness.", + "parameters": { + "properties": { + "org_id": { + "description": "Required ID of the organization.", + "type": "string" + }, + "project_id": { + "description": "Required ID of the project.", + "type": "string" + }, + "template_identifier": { + "description": "The identifier of the template", + "type": "string" + } + }, + "type": "object" + } + } + ] +} diff --git a/client/client.go b/client/client.go index b4b390a..515bb66 100644 --- a/client/client.go +++ b/client/client.go @@ -52,6 +52,7 @@ type Client struct { PullRequests *PullRequestService Pipelines *PipelineService Repositories *RepositoryService + Templates *TemplateService Logs *LogService Registry *ar.ClientWithResponses } @@ -97,6 +98,7 @@ func (c *Client) initialize() error { c.PullRequests = &PullRequestService{client: c} c.Pipelines = &PipelineService{client: c} c.Repositories = &RepositoryService{client: c} + c.Templates = &TemplateService{client: c} c.Logs = &LogService{client: c} // TODO: Replace it with harness-go-sdk diff --git a/client/dto/template.go b/client/dto/template.go new file mode 100644 index 0000000..78b3eb8 --- /dev/null +++ b/client/dto/template.go @@ -0,0 +1,61 @@ +package dto + +// TemplateListOptions represents the options for listing templates +type TemplateListOptions struct { + PaginationOptions + SearchTerm string `json:"searchTerm,omitempty"` + TemplateListType string `json:"templateListType,omitempty"` + Sort string `json:"sort,omitempty"` +} + +// TemplateListItem represents an item in the template list +type TemplateListItem struct { + Name string `json:"name,omitempty"` + Identifier string `json:"identifier,omitempty"` + Description string `json:"description,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Version int `json:"version,omitempty"` + CreatedAt int64 `json:"createdAt,omitempty"` + LastUpdatedAt int64 `json:"lastUpdatedAt,omitempty"` + Type string `json:"type,omitempty"` + StoreType string `json:"storeType,omitempty"` + VersionLabel string `json:"versionLabel,omitempty"` + ConnectorRef string `json:"connectorRef,omitempty"` +} + +// TemplateData represents the data field of a template response +type TemplateData struct { + Name string `json:"name,omitempty"` + Identifier string `json:"identifier,omitempty"` + Description string `json:"description,omitempty"` + YamlContent string `json:"yamlContent,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + Version int `json:"version,omitempty"` + CreatedAt int64 `json:"createdAt,omitempty"` + LastUpdatedAt int64 `json:"lastUpdatedAt,omitempty"` + Type string `json:"type,omitempty"` + StoreType string `json:"storeType,omitempty"` + VersionLabel string `json:"versionLabel,omitempty"` + ConnectorRef string `json:"connectorRef,omitempty"` +} + +// TemplateCreate represents the data needed to create a template +type TemplateCreate struct { + Name string `json:"name"` + Identifier string `json:"identifier"` + Description string `json:"description,omitempty"` + YamlContent string `json:"yamlContent"` + Tags map[string]string `json:"tags,omitempty"` + Type string `json:"type"` + StoreType string `json:"storeType,omitempty"` + VersionLabel string `json:"versionLabel,omitempty"` +} + +// TemplateUpdate represents the data needed to update a template +type TemplateUpdate struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + YamlContent string `json:"yamlContent,omitempty"` + Tags map[string]string `json:"tags,omitempty"` + VersionLabel string `json:"versionLabel,omitempty"` +} \ No newline at end of file diff --git a/client/templates.go b/client/templates.go new file mode 100644 index 0000000..4eb8305 --- /dev/null +++ b/client/templates.go @@ -0,0 +1,96 @@ +package client + +import ( + "context" + "fmt" + + "github.com/harness/harness-mcp/client/dto" +) + +const ( + templateBasePath = "gateway/template/api/templates/%s" + templateListPath = "gateway/template/api/templates/list-metadata" +) + +type TemplateService struct { + client *Client +} + +func (t *TemplateService) Get(ctx context.Context, scope dto.Scope, templateID string) ( + *dto.Entity[dto.TemplateData], + error, +) { + path := fmt.Sprintf(templateBasePath, templateID) + + // Prepare query parameters + params := make(map[string]string) + addScope(scope, params) + + // Initialize the response object + response := &dto.Entity[dto.TemplateData]{} + + // Make the GET request + err := t.client.Get(ctx, path, params, map[string]string{}, response) + if err != nil { + return nil, fmt.Errorf("failed to get template: %w", err) + } + + return response, nil +} + +func (t *TemplateService) List( + ctx context.Context, + scope dto.Scope, + opts *dto.TemplateListOptions, +) (*dto.ListOutput[dto.TemplateListItem], error) { + // Prepare query parameters + params := make(map[string]string) + addScope(scope, params) + + // Handle nil options by creating default options + if opts == nil { + opts = &dto.TemplateListOptions{} + } + + // Set default pagination + setDefaultPagination(&opts.PaginationOptions) + + // Add pagination parameters + params["page"] = fmt.Sprintf("%d", opts.Page) + params["size"] = fmt.Sprintf("%d", opts.Size) + + // Set templateListType parameter with default if not provided + if opts.TemplateListType != "" { + params["templateListType"] = opts.TemplateListType + } else { + params["templateListType"] = "LastUpdated" + } + + // Set sort parameter with default if not provided + if opts.Sort != "" { + params["sort"] = opts.Sort + } else { + params["sort"] = "lastUpdatedAt,DESC" + } + + // Add optional parameters if provided + if opts.SearchTerm != "" { + params["searchTerm"] = opts.SearchTerm + } + + // Create request body - this is required for templates + requestBody := map[string]string{ + "filterType": "Template", + } + + // Initialize the response object + response := &dto.ListOutput[dto.TemplateListItem]{} + + // Make the POST request + err := t.client.Post(ctx, templateListPath, params, requestBody, response) + if err != nil { + return nil, fmt.Errorf("failed to list templates: %w", err) + } + + return response, nil +} \ No newline at end of file diff --git a/pkg/harness/templates.go b/pkg/harness/templates.go new file mode 100644 index 0000000..5160b28 --- /dev/null +++ b/pkg/harness/templates.go @@ -0,0 +1,139 @@ +package harness + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/harness/harness-mcp/client" + "github.com/harness/harness-mcp/client/dto" + "github.com/harness/harness-mcp/cmd/harness-mcp-server/config" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func GetTemplateTool(config *config.Config, client *client.Client) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("get_template", + mcp.WithDescription("Get details of a specific template in Harness."), + mcp.WithString("template_identifier", + mcp.Description("The identifier of the template"), + mcp.Required(), + ), + WithScope(config, true), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + templateID, err := OptionalParam[string](request, "template_identifier") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + if templateID == "" { + return mcp.NewToolResultError("template_identifier is required"), nil + } + + scope, err := fetchScope(config, request, true) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + data, err := client.Templates.Get(ctx, scope, templateID) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to get template: %s", err)), nil + } + + r, err := json.Marshal(data.Data) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal template: %s", err)), nil + } + + return mcp.NewToolResultText(string(r)), nil + } +} + +func ListTemplatesTool(config *config.Config, client *client.Client) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_templates", + mcp.WithDescription("List templates in Harness."), + mcp.WithNumber("page", + mcp.DefaultNumber(0), + mcp.Description("Page number for pagination - page 0 is the first page"), + ), + mcp.WithNumber("size", + mcp.DefaultNumber(5), + mcp.Max(20), + mcp.Description("Number of items per page"), + ), + mcp.WithString("search", + mcp.Description("Optional search term to filter templates"), + ), + mcp.WithString("template_list_type", + mcp.Description("Optional type of template list (e.g., LastUpdated)"), + ), + mcp.WithString("sort", + mcp.Description("Optional sort order (e.g., lastUpdatedAt,DESC)"), + ), + WithScope(config, true), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + page, err := OptionalParam[float64](request, "page") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + // Default is already 0, no need to set it + size, err := OptionalParam[float64](request, "size") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + // Use default if value not provided + if size == 0 { + size = 5 // Default value + } + search, err := OptionalParam[string](request, "search") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + // Default is empty string, no need to set it + templateListType, err := OptionalParam[string](request, "template_list_type") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + // Use default if value not provided + if templateListType == "" { + templateListType = "LastUpdated" // Default value + } + sort, err := OptionalParam[string](request, "sort") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + // Use default if value not provided + if sort == "" { + sort = "lastUpdatedAt,DESC" // Default value + } + scope, err := fetchScope(config, request, true) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Create the options for listing templates + options := &dto.TemplateListOptions{ + PaginationOptions: dto.PaginationOptions{ + Page: int(page), + Size: int(size), + }, + SearchTerm: search, + TemplateListType: templateListType, + Sort: sort, + } + + data, err := client.Templates.List(ctx, scope, options) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to list templates: %s", err)), nil + } + + r, err := json.Marshal(data.Data) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal templates: %s", err)), nil + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/harness/tools.go b/pkg/harness/tools.go index b2cfa9e..8193880 100644 --- a/pkg/harness/tools.go +++ b/pkg/harness/tools.go @@ -53,6 +53,13 @@ func InitToolsets(client *client.Client, config *config.Config) (*toolsets.Tools toolsets.NewServerTool(ListArtifactFilesTool(config, client)), ) + // Create the templates toolset + templates := toolsets.NewToolset("templates", "Harness Template related tools"). + AddReadTools( + toolsets.NewServerTool(ListTemplatesTool(config, client)), + toolsets.NewServerTool(GetTemplateTool(config, client)), + ) + // Create the logs toolset logs := toolsets.NewToolset("logs", "Harness Logs related tools"). AddReadTools( @@ -64,6 +71,7 @@ func InitToolsets(client *client.Client, config *config.Config) (*toolsets.Tools tsg.AddToolset(pipelines) tsg.AddToolset(repositories) tsg.AddToolset(registries) + tsg.AddToolset(templates) tsg.AddToolset(logs) // Enable requested toolsets From 7c1929d4ddf91d4f2937bb8d1a977c3d13453954 Mon Sep 17 00:00:00 2001 From: Utkarsh Choubey Date: Fri, 23 May 2025 17:02:38 +0530 Subject: [PATCH 2/2] feat: [cds-0]: removed unused files --- .cascade-mcp-config.json | 65 ---------------------------------------- 1 file changed, 65 deletions(-) delete mode 100644 .cascade-mcp-config.json diff --git a/.cascade-mcp-config.json b/.cascade-mcp-config.json deleted file mode 100644 index 0ad79b3..0000000 --- a/.cascade-mcp-config.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "tools": [ - { - "name": "mcp0_list_templates", - "description": "List templates in Harness.", - "parameters": { - "properties": { - "org_id": { - "description": "Required ID of the organization.", - "type": "string" - }, - "project_id": { - "description": "Required ID of the project.", - "type": "string" - }, - "page": { - "description": "Page number for pagination - page 0 is the first page", - "default": 0, - "type": "number" - }, - "size": { - "description": "Number of items per page", - "default": 5, - "maximum": 20, - "type": "number" - }, - "search": { - "description": "Optional search term to filter templates", - "type": "string" - }, - "template_list_type": { - "description": "Optional type of template list (e.g., LastUpdated)", - "type": "string" - }, - "sort": { - "description": "Optional sort order (e.g., lastUpdatedAt,DESC)", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "mcp0_get_template", - "description": "Get details of a specific template in Harness.", - "parameters": { - "properties": { - "org_id": { - "description": "Required ID of the organization.", - "type": "string" - }, - "project_id": { - "description": "Required ID of the project.", - "type": "string" - }, - "template_identifier": { - "description": "The identifier of the template", - "type": "string" - } - }, - "type": "object" - } - } - ] -}