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