Skip to content

Commit c4a9496

Browse files
rgrassian-splitHarness
authored andcommitted
feat: [FME-10086]: FME toolset (#220)
* 5cd34c format * ae8401 Apply suggestion from code review * 807910 remove not needed file * c99c5c remove not needed file * da2e6f FME toolset (#222) * 673a71 FME toolset (#221) * 208d58 fix auth * 4ba96a add missing files * db2381 first pass at fme implementation
1 parent a050658 commit c4a9496

File tree

7 files changed

+446
-0
lines changed

7 files changed

+446
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,15 @@ Toolset Name: `audit`
244244

245245
- `list_user_audits`: Retrieve the complete audit trail for a specified user.
246246

247+
#### Feature Management and Experimentation (FME) Toolset
248+
249+
Toolset Name: `fme`
250+
251+
- `list_fme_workspaces`: List all FME workspaces
252+
- `list_fme_environments`: List environments for a specific workspace
253+
- `list_fme_feature_flags`: List feature flags for a specific workspace
254+
- `get_fme_feature_flag_definition`: Get the definition of a specific feature flag in an environment
255+
247256

248257
## Prerequisites
249258

client/dto/fme.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package dto
2+
3+
// FME (Feature Management and Experimentation) DTOs for Split.io APIs
4+
// Based on actual API response examples from Split.io
5+
6+
// FMEWorkspace represents a workspace in Split.io
7+
type FMEWorkspace struct {
8+
ID string `json:"id"`
9+
Name string `json:"name"`
10+
}
11+
12+
// FMEWorkspacesResponse represents the response from listing workspaces
13+
// GET /internal/api/v2/workspaces
14+
type FMEWorkspacesResponse struct {
15+
Objects []FMEWorkspace `json:"objects"`
16+
Offset int `json:"offset"`
17+
Limit int `json:"limit"`
18+
TotalCount int `json:"totalCount"`
19+
}
20+
21+
// FMEEnvironment represents an environment in Split.io
22+
type FMEEnvironment struct {
23+
ID string `json:"id"`
24+
Name string `json:"name"`
25+
}
26+
27+
// FMEEnvironmentsResponse represents the response from listing environments
28+
// GET /internal/api/v2/environments/ws/{wsId}
29+
// Note: This endpoint returns a simple array, not a paginated object
30+
type FMEEnvironmentsResponse []FMEEnvironment
31+
32+
// FMETrafficType represents a traffic type in Split.io
33+
type FMETrafficType struct {
34+
ID string `json:"id"`
35+
Name string `json:"name"`
36+
}
37+
38+
// FMETag represents a tag in Split.io
39+
type FMETag struct {
40+
Name string `json:"name"`
41+
}
42+
43+
// FMERolloutStatus represents the rollout status of a feature flag
44+
type FMERolloutStatus struct {
45+
ID string `json:"id"`
46+
Name string `json:"name"`
47+
}
48+
49+
// FMEFeatureFlag represents a feature flag (split) in Split.io
50+
type FMEFeatureFlag struct {
51+
Name string `json:"name"`
52+
Description string `json:"description"`
53+
ID string `json:"id"`
54+
TrafficType FMETrafficType `json:"trafficType"`
55+
CreationTime int64 `json:"creationTime"`
56+
Tags []FMETag `json:"tags"`
57+
RolloutStatus FMERolloutStatus `json:"rolloutStatus"`
58+
RolloutStatusTimestamp int64 `json:"rolloutStatusTimestamp"`
59+
}
60+
61+
// FMEFeatureFlagsResponse represents the response from listing feature flags
62+
// GET /internal/api/v2/splits/ws/{wsId}
63+
type FMEFeatureFlagsResponse struct {
64+
Objects []FMEFeatureFlag `json:"objects"`
65+
Offset int `json:"offset"`
66+
Limit int `json:"limit"`
67+
TotalCount int `json:"totalCount"`
68+
}
69+
70+
// FMETreatment represents a treatment (variation) in a feature flag
71+
type FMETreatment struct {
72+
Name string `json:"name"`
73+
Configurations string `json:"configurations"`
74+
}
75+
76+
// FMEBucket represents a bucket in a rule
77+
type FMEBucket struct {
78+
Treatment string `json:"treatment"`
79+
Size int `json:"size"`
80+
}
81+
82+
// FMEMatcher represents a matcher in a rule condition
83+
type FMEMatcher struct {
84+
Type string `json:"type"`
85+
String string `json:"string"`
86+
}
87+
88+
// FMECondition represents a condition in a rule
89+
type FMECondition struct {
90+
Combiner string `json:"combiner"`
91+
Matchers []FMEMatcher `json:"matchers"`
92+
}
93+
94+
// FMERule represents a targeting rule in a feature flag
95+
type FMERule struct {
96+
Buckets []FMEBucket `json:"buckets"`
97+
Condition FMECondition `json:"condition"`
98+
}
99+
100+
// FMEDefaultRule represents a default rule bucket
101+
type FMEDefaultRule struct {
102+
Treatment string `json:"treatment"`
103+
Size int `json:"size"`
104+
}
105+
106+
// FMEFeatureFlagDefinition represents the complete definition of a feature flag in an environment
107+
type FMEFeatureFlagDefinition struct {
108+
Name string `json:"name"`
109+
Environment FMEEnvironment `json:"environment"`
110+
TrafficType FMETrafficType `json:"trafficType"`
111+
Killed bool `json:"killed"`
112+
LastTrafficReceivedAt int64 `json:"lastTrafficReceivedAt"`
113+
Treatments []FMETreatment `json:"treatments"`
114+
DefaultTreatment string `json:"defaultTreatment"`
115+
BaselineTreatment string `json:"baselineTreatment"`
116+
TrafficAllocation int `json:"trafficAllocation"`
117+
Rules []FMERule `json:"rules"`
118+
DefaultRule []FMEDefaultRule `json:"defaultRule"`
119+
CreationTime int64 `json:"creationTime"`
120+
LastUpdateTime int64 `json:"lastUpdateTime"`
121+
}
122+
123+
// FMEFeatureFlagDefinitionResponse represents the response from getting a feature flag definition
124+
// GET /internal/api/v2/splits/ws/{wsId}/{feature_flag_name}/environments/{environment_id_or_name}
125+
// Note: This endpoint returns the definition object directly, not wrapped
126+
type FMEFeatureFlagDefinitionResponse = FMEFeatureFlagDefinition

client/fme.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/harness/harness-mcp/client/dto"
8+
)
9+
10+
// FMEService provides access to Split.io FME APIs
11+
type FMEService struct {
12+
Client *Client
13+
}
14+
15+
// ListWorkspaces retrieves all FME workspaces
16+
// GET https://api.split.io/internal/api/v2/workspaces
17+
func (f *FMEService) ListWorkspaces(ctx context.Context) (*dto.FMEWorkspacesResponse, error) {
18+
var response dto.FMEWorkspacesResponse
19+
20+
err := f.Client.Get(ctx, "internal/api/v2/workspaces", nil, nil, &response)
21+
if err != nil {
22+
return nil, fmt.Errorf("failed to list workspaces: %w", err)
23+
}
24+
25+
return &response, nil
26+
}
27+
28+
// ListEnvironments retrieves all environments for a specific workspace
29+
// GET https://api.split.io/internal/api/v2/environments/ws/{wsId}
30+
func (f *FMEService) ListEnvironments(ctx context.Context, wsID string) (*dto.FMEEnvironmentsResponse, error) {
31+
var response dto.FMEEnvironmentsResponse
32+
33+
path := fmt.Sprintf("internal/api/v2/environments/ws/%s", wsID)
34+
err := f.Client.Get(ctx, path, nil, nil, &response)
35+
if err != nil {
36+
return nil, fmt.Errorf("failed to list environments: %w", err)
37+
}
38+
39+
return &response, nil
40+
}
41+
42+
// ListFeatureFlags retrieves all feature flags for a specific workspace
43+
// GET https://api.split.io/internal/api/v2/splits/ws/{wsId}
44+
func (f *FMEService) ListFeatureFlags(ctx context.Context, wsID string) (*dto.FMEFeatureFlagsResponse, error) {
45+
var response dto.FMEFeatureFlagsResponse
46+
47+
path := fmt.Sprintf("internal/api/v2/splits/ws/%s", wsID)
48+
err := f.Client.Get(ctx, path, nil, nil, &response)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to list feature flags: %w", err)
51+
}
52+
53+
return &response, nil
54+
}
55+
56+
// GetFeatureFlagDefinition retrieves a specific feature flag definition
57+
// GET https://api.split.io/internal/api/v2/splits/ws/{wsId}/{feature_flag_name}/environments/{environment_id_or_name}
58+
func (f *FMEService) GetFeatureFlagDefinition(ctx context.Context, wsID, flagName, environmentIDOrName string) (*dto.FMEFeatureFlagDefinitionResponse, error) {
59+
var response dto.FMEFeatureFlagDefinitionResponse
60+
61+
path := fmt.Sprintf("internal/api/v2/splits/ws/%s/%s/environments/%s", wsID, flagName, environmentIDOrName)
62+
err := f.Client.Get(ctx, path, nil, nil, &response)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to get feature flag definition: %w", err)
65+
}
66+
67+
return &response, nil
68+
}

pkg/harness/tools.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,9 @@ func initLegacyToolsets(config *config.Config, tsg *toolsets.ToolsetGroup) error
291291
if err := modules.RegisterSoftwareEngineeringInsights(config, tsg); err != nil {
292292
return err
293293
}
294+
if err := modules.RegisterFeatureManagementAndExperimentation(config, tsg); err != nil {
295+
return err
296+
}
294297
} else {
295298
// Register specified toolsets
296299
for _, toolset := range config.Toolsets {
@@ -411,6 +414,10 @@ func initLegacyToolsets(config *config.Config, tsg *toolsets.ToolsetGroup) error
411414
if err := modules.RegisterSoftwareEngineeringInsights(config, tsg); err != nil {
412415
return err
413416
}
417+
case "fme":
418+
if err := modules.RegisterFeatureManagementAndExperimentation(config, tsg); err != nil {
419+
return err
420+
}
414421
}
415422
}
416423
}

pkg/harness/tools/fme.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
// ListFMEWorkspacesTool creates a tool for listing FME workspaces
15+
func ListFMEWorkspacesTool(config *config.Config, fmeService *client.FMEService) (mcp.Tool, server.ToolHandlerFunc) {
16+
return mcp.NewTool("list_fme_workspaces",
17+
mcp.WithDescription("List Feature Management & Experimentation (FME) workspaces."),
18+
),
19+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
20+
workspaces, err := fmeService.ListWorkspaces(ctx)
21+
if err != nil {
22+
return mcp.NewToolResultError(fmt.Sprintf("failed to list FME workspaces: %v", err)), nil
23+
}
24+
25+
responseBytes, err := json.Marshal(workspaces)
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to marshal workspaces: %w", err)
28+
}
29+
30+
return mcp.NewToolResultText(string(responseBytes)), nil
31+
}
32+
}
33+
34+
// ListFMEEnvironmentsTool creates a tool for listing FME environments for a specific workspace
35+
func ListFMEEnvironmentsTool(config *config.Config, fmeService *client.FMEService) (mcp.Tool, server.ToolHandlerFunc) {
36+
return mcp.NewTool("list_fme_environments",
37+
mcp.WithDescription("List Feature Management & Experimentation (FME) environments for a specific workspace."),
38+
mcp.WithString("ws_id",
39+
mcp.Required(),
40+
mcp.Description("The workspace ID to list environments for"),
41+
),
42+
),
43+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
44+
wsID, err := RequiredParam[string](request, "ws_id")
45+
if err != nil {
46+
return mcp.NewToolResultError(err.Error()), nil
47+
}
48+
49+
environments, err := fmeService.ListEnvironments(ctx, wsID)
50+
if err != nil {
51+
return mcp.NewToolResultError(fmt.Sprintf("failed to list FME environments: %v", err)), nil
52+
}
53+
54+
responseBytes, err := json.Marshal(environments)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to marshal environments: %w", err)
57+
}
58+
59+
return mcp.NewToolResultText(string(responseBytes)), nil
60+
}
61+
}
62+
63+
// ListFMEFeatureFlagsTool creates a tool for listing FME feature flags for a specific workspace
64+
func ListFMEFeatureFlagsTool(config *config.Config, fmeService *client.FMEService) (mcp.Tool, server.ToolHandlerFunc) {
65+
return mcp.NewTool("list_fme_feature_flags",
66+
mcp.WithDescription("List Feature Management & Experimentation (FME) feature flags for a specific workspace."),
67+
mcp.WithString("ws_id",
68+
mcp.Required(),
69+
mcp.Description("The workspace ID to list feature flags for"),
70+
),
71+
),
72+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
73+
wsID, err := RequiredParam[string](request, "ws_id")
74+
if err != nil {
75+
return mcp.NewToolResultError(err.Error()), nil
76+
}
77+
78+
featureFlags, err := fmeService.ListFeatureFlags(ctx, wsID)
79+
if err != nil {
80+
return mcp.NewToolResultError(fmt.Sprintf("failed to list FME feature flags: %v", err)), nil
81+
}
82+
83+
responseBytes, err := json.Marshal(featureFlags)
84+
if err != nil {
85+
return nil, fmt.Errorf("failed to marshal feature flags: %w", err)
86+
}
87+
88+
return mcp.NewToolResultText(string(responseBytes)), nil
89+
}
90+
}
91+
92+
// GetFMEFeatureFlagDefinitionTool creates a tool for getting a specific FME feature flag definition
93+
func GetFMEFeatureFlagDefinitionTool(config *config.Config, fmeService *client.FMEService) (mcp.Tool, server.ToolHandlerFunc) {
94+
return mcp.NewTool("get_fme_feature_flag_definition",
95+
mcp.WithDescription("Get the definition of a specific Feature Management & Experimentation (FME) feature flag in an environment."),
96+
mcp.WithString("ws_id",
97+
mcp.Required(),
98+
mcp.Description("The workspace ID"),
99+
),
100+
mcp.WithString("feature_flag_name",
101+
mcp.Required(),
102+
mcp.Description("The name of the feature flag"),
103+
),
104+
mcp.WithString("environment_id_or_name",
105+
mcp.Required(),
106+
mcp.Description("The environment ID or name"),
107+
),
108+
),
109+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
110+
wsID, err := RequiredParam[string](request, "ws_id")
111+
if err != nil {
112+
return mcp.NewToolResultError(err.Error()), nil
113+
}
114+
115+
flagName, err := RequiredParam[string](request, "feature_flag_name")
116+
if err != nil {
117+
return mcp.NewToolResultError(err.Error()), nil
118+
}
119+
120+
envIDOrName, err := RequiredParam[string](request, "environment_id_or_name")
121+
if err != nil {
122+
return mcp.NewToolResultError(err.Error()), nil
123+
}
124+
125+
definition, err := fmeService.GetFeatureFlagDefinition(ctx, wsID, flagName, envIDOrName)
126+
if err != nil {
127+
return mcp.NewToolResultError(fmt.Sprintf("failed to get FME feature flag definition: %v", err)), nil
128+
}
129+
130+
responseBytes, err := json.Marshal(definition)
131+
if err != nil {
132+
return nil, fmt.Errorf("failed to marshal feature flag definition: %w", err)
133+
}
134+
135+
return mcp.NewToolResultText(string(responseBytes)), nil
136+
}
137+
}

0 commit comments

Comments
 (0)