Skip to content

Commit 9def781

Browse files
Saranya-jenaHarness
authored andcommitted
chore: [ML-1320]: Added get_secret and list_settings tools and e2e tests (#145)
* chore: [ML-1320]: Added get_secret and list_settings tools and e2e tests Signed-off-by: Saranya-jena <[email protected]>
1 parent 332ef6f commit 9def781

File tree

12 files changed

+477
-20
lines changed

12 files changed

+477
-20
lines changed

client/dto/secrets.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package dto
2+
3+
// SecretResponse represents the response from the Harness secrets API
4+
type SecretResponse struct {
5+
Status string `json:"status"`
6+
Data SecretData `json:"data"`
7+
MetaData interface{} `json:"metaData"`
8+
CorrelationID string `json:"correlationId"`
9+
}
10+
11+
// SecretData represents the data field in the secret response
12+
type SecretData struct {
13+
Secret Secret `json:"secret"`
14+
CreatedAt int64 `json:"createdAt"`
15+
UpdatedAt int64 `json:"updatedAt"`
16+
Draft bool `json:"draft"`
17+
}
18+
19+
// Secret represents the details of a Harness secret
20+
type Secret struct {
21+
Type string `json:"type"`
22+
Name string `json:"name"`
23+
Identifier string `json:"identifier"`
24+
Tags map[string]string `json:"tags"`
25+
Description string `json:"description"`
26+
Spec SecretSpec `json:"spec"`
27+
}
28+
29+
// SecretSpec represents the specification of a secret
30+
type SecretSpec struct {
31+
SecretManagerIdentifier string `json:"secretManagerIdentifier"`
32+
AdditionalMetadata interface{} `json:"additionalMetadata"`
33+
}
34+
35+
// SecretGetOptions represents options for getting a secret
36+
type SecretGetOptions struct {
37+
// Add any additional options needed for getting secrets
38+
}

client/dto/settings.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dto
2+
3+
// SettingItem represents a single setting in the Harness settings API response
4+
type SettingItem struct {
5+
Setting Setting `json:"setting"`
6+
LastModifiedAt *int64 `json:"lastModifiedAt"`
7+
}
8+
9+
// Setting represents the details of a Harness setting
10+
type Setting struct {
11+
Identifier string `json:"identifier"`
12+
Name string `json:"name"`
13+
OrgIdentifier *string `json:"orgIdentifier"`
14+
ProjectIdentifier *string `json:"projectIdentifier"`
15+
Category string `json:"category"`
16+
GroupIdentifier string `json:"groupIdentifier"`
17+
ValueType string `json:"valueType"`
18+
AllowedValues []string `json:"allowedValues"`
19+
AllowOverrides bool `json:"allowOverrides"`
20+
Value *string `json:"value"`
21+
DefaultValue *string `json:"defaultValue"`
22+
SettingSource string `json:"settingSource"`
23+
IsSettingEditable bool `json:"isSettingEditable"`
24+
AllowedScopes []string `json:"allowedScopes"`
25+
}
26+
27+
// SettingsResponse represents the response from the Harness settings API
28+
type SettingsResponse struct {
29+
Status string `json:"status"`
30+
Data []SettingItem `json:"data"`
31+
MetaData interface{} `json:"metaData"`
32+
CorrelationID string `json:"correlationId"`
33+
}
34+
35+
// SettingsListOptions represents options for listing settings
36+
type SettingsListOptions struct {
37+
Category string
38+
Group string
39+
IncludeParentScopes bool
40+
PaginationOptions
41+
}

client/secrets.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/harness/harness-mcp/client/dto"
8+
)
9+
10+
const (
11+
secretsBasePath = "/v2/secrets"
12+
)
13+
14+
// SecretsClient provides methods to interact with the Harness secrets API
15+
type SecretsClient struct {
16+
*Client
17+
}
18+
19+
// GetSecret retrieves a secret by its identifier
20+
func (c *SecretsClient) GetSecret(ctx context.Context, scope dto.Scope, secretIdentifier string) (*dto.SecretResponse, error) {
21+
if scope.AccountID == "" {
22+
return nil, fmt.Errorf("accountIdentifier cannot be null")
23+
}
24+
25+
path := fmt.Sprintf("%s/%s", secretsBasePath, secretIdentifier)
26+
params := make(map[string]string)
27+
addScope(scope, params)
28+
29+
var response dto.SecretResponse
30+
err := c.Get(ctx, path, params, nil, &response)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to get secret: %w", err)
33+
}
34+
35+
return &response, nil
36+
}

client/settings.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/harness/harness-mcp/client/dto"
9+
)
10+
11+
const (
12+
settingsListPath = "/settings"
13+
)
14+
15+
// SettingsClient provides methods to interact with the Harness settings API
16+
type SettingsClient struct {
17+
*Client
18+
}
19+
20+
// List retrieves settings based on the provided scope and options
21+
func (c *SettingsClient) List(ctx context.Context, scope dto.Scope, opts *dto.SettingsListOptions) (*dto.SettingsResponse, error) {
22+
path := settingsListPath
23+
params := make(map[string]string)
24+
if scope.AccountID == "" {
25+
return nil, fmt.Errorf("accountIdentifier cannot be null")
26+
}
27+
addScope(scope, params)
28+
29+
if opts != nil {
30+
if opts.Category != "" {
31+
params["category"] = opts.Category
32+
}
33+
if opts.Group != "" {
34+
params["group"] = opts.Group
35+
}
36+
if opts.IncludeParentScopes {
37+
params["includeParentScopes"] = "true"
38+
}
39+
// Add pagination parameters
40+
if opts.Page > 0 {
41+
params["page"] = strconv.Itoa(opts.Page)
42+
}
43+
if opts.Size > 0 {
44+
params["size"] = strconv.Itoa(opts.Size)
45+
}
46+
}
47+
48+
var response dto.SettingsResponse
49+
err := c.Get(ctx, path, params, nil, &response)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to list settings: %w", err)
52+
}
53+
54+
return &response, nil
55+
}

pkg/harness/tools.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ func initLegacyToolsets(config *config.Config, tsg *toolsets.ToolsetGroup) error
274274
if err := modules.RegisterACM(config, tsg); err != nil {
275275
return err
276276
}
277+
if err := modules.RegisterSettings(config, tsg); err != nil {
278+
return err
279+
}
280+
if err := modules.RegisterSecrets(config, tsg); err != nil {
281+
return err
282+
}
277283
} else {
278284
// Register specified toolsets
279285
for _, toolset := range config.Toolsets {
@@ -374,6 +380,14 @@ func initLegacyToolsets(config *config.Config, tsg *toolsets.ToolsetGroup) error
374380
if err := modules.RegisterACM(config, tsg); err != nil {
375381
return err
376382
}
383+
case "settings":
384+
if err := modules.RegisterSettings(config, tsg); err != nil {
385+
return err
386+
}
387+
case "secrets":
388+
if err := modules.RegisterSecrets(config, tsg); err != nil {
389+
return err
390+
}
377391
}
378392
}
379393
}

pkg/harness/tools/secrets.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/harness/harness-mcp/client"
9+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
// GetSecretTool creates a tool for retrieving a secret from Harness
15+
func GetSecretTool(config *config.Config, client *client.SecretsClient) (tool mcp.Tool, handler server.ToolHandlerFunc) {
16+
return mcp.NewTool("get_secret",
17+
mcp.WithDescription("Get a secret by identifier from Harness."),
18+
mcp.WithString("secret_identifier",
19+
mcp.Required(),
20+
mcp.Description("Identifier of the secret to retrieve"),
21+
),
22+
WithScope(config, false),
23+
),
24+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
25+
scope, err := FetchScope(config, request, false)
26+
if err != nil {
27+
return mcp.NewToolResultError(err.Error()), nil
28+
}
29+
30+
secretIdentifier, err := RequiredParam[string](request, "secret_identifier")
31+
if err != nil {
32+
return mcp.NewToolResultError(err.Error()), nil
33+
}
34+
35+
// Call the client to get the secret
36+
response, err := client.GetSecret(ctx, scope, secretIdentifier)
37+
if err != nil {
38+
return nil, fmt.Errorf("failed to get secret: %w", err)
39+
}
40+
41+
r, err := json.Marshal(response)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to marshal secret response: %w", err)
44+
}
45+
46+
return mcp.NewToolResultText(string(r)), nil
47+
}
48+
}

pkg/harness/tools/settings.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/harness/harness-mcp/client"
8+
"github.com/harness/harness-mcp/client/dto"
9+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
// ListSettingsTool creates a tool for listing settings in Harness
15+
func ListSettingsTool(config *config.Config, client *client.SettingsClient) (tool mcp.Tool, handler server.ToolHandlerFunc) {
16+
return mcp.NewTool("list_settings",
17+
mcp.WithDescription("List settings in Harness with filtering options by category and group."),
18+
mcp.WithString("category",
19+
mcp.Required(),
20+
mcp.Description("Category to filter settings. Available categories: 'CD', 'CI', 'CE', 'CV', 'CF', 'STO', 'CORE', 'PMS', 'TEMPLATESERVICE', 'GOVERNANCE', 'CHAOS', 'SCIM', 'GIT_EXPERIENCE', 'CONNECTORS', 'EULA', 'NOTIFICATIONS', 'SUPPLY_CHAIN_ASSURANCE', 'USER', 'MODULES_VISIBILITY', 'DBOPS', 'IR'"),
21+
),
22+
mcp.WithString("group",
23+
mcp.Description("Optional group to filter settings within a category"),
24+
),
25+
mcp.WithBoolean("include_parent_scopes",
26+
mcp.Description("Flag to include the settings which only exist at the parent scopes"),
27+
),
28+
WithScope(config, false),
29+
WithPagination(),
30+
),
31+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
32+
scope, err := FetchScope(config, request, false)
33+
if err != nil {
34+
return mcp.NewToolResultError(err.Error()), nil
35+
}
36+
37+
page, size, err := FetchPagination(request)
38+
if err != nil {
39+
return mcp.NewToolResultError(err.Error()), nil
40+
}
41+
42+
category, err := OptionalParam[string](request, "category")
43+
if err != nil {
44+
return mcp.NewToolResultError(err.Error()), nil
45+
}
46+
47+
group, err := OptionalParam[string](request, "group")
48+
if err != nil {
49+
return mcp.NewToolResultError(err.Error()), nil
50+
}
51+
52+
// Default includeParentScopes to true
53+
includeParentScopes := true
54+
55+
// Check if parameter was provided
56+
includeParentScopesParam, err := OptionalParam[bool](request, "include_parent_scopes")
57+
if err == nil {
58+
includeParentScopes = includeParentScopesParam
59+
}
60+
61+
// Call the client to get the settings
62+
opts := &dto.SettingsListOptions{
63+
Category: category,
64+
Group: group,
65+
IncludeParentScopes: includeParentScopes,
66+
PaginationOptions: dto.PaginationOptions{
67+
Page: page,
68+
Size: size,
69+
},
70+
}
71+
72+
response, err := client.List(ctx, scope, opts)
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to list settings: %w", err)
75+
}
76+
77+
r, err := json.Marshal(response)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to marshal settings list: %w", err)
80+
}
81+
82+
return mcp.NewToolResultText(string(r)), nil
83+
}
84+
}

0 commit comments

Comments
 (0)