Skip to content

feat: [PIPE-1]: Add Support for Templates Get and List Call #4

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Client struct {
PullRequests *PullRequestService
Pipelines *PipelineService
Repositories *RepositoryService
Templates *TemplateService
Logs *LogService
Registry *ar.ClientWithResponses
}
Expand Down Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions client/dto/template.go
Original file line number Diff line number Diff line change
@@ -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"`
}
96 changes: 96 additions & 0 deletions client/templates.go
Original file line number Diff line number Diff line change
@@ -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
}
139 changes: 139 additions & 0 deletions pkg/harness/templates.go
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions pkg/harness/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down