Skip to content

Commit 2d7f57d

Browse files
committed
Add list_clusters tool
1 parent e5a4d8d commit 2d7f57d

File tree

5 files changed

+541
-31
lines changed

5 files changed

+541
-31
lines changed

internal/client/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ func (c *Client) ReadyConn(ctx context.Context) (*grpc.ClientConn, error) {
143143
return c.conn, nil
144144
}
145145

146+
// SetConnForTesting sets a gRPC connection for testing purposes.
147+
// This should only be used in tests.
148+
func (c *Client) SetConnForTesting(conn *grpc.ClientConn) {
149+
c.mu.Lock()
150+
defer c.mu.Unlock()
151+
152+
c.conn = conn
153+
c.connected = true
154+
}
155+
146156
func (c *Client) shouldRedialNoLock() bool {
147157
if !c.connected || c.conn == nil {
148158
return true

internal/toolsets/config/tools.go

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,44 @@ package config
22

33
import (
44
"context"
5-
"fmt"
65

6+
"github.com/google/jsonschema-go/jsonschema"
77
"github.com/modelcontextprotocol/go-sdk/mcp"
88
"github.com/pkg/errors"
99
v1 "github.com/stackrox/rox/generated/api/v1"
1010
"github.com/stackrox/stackrox-mcp/internal/client"
1111
"github.com/stackrox/stackrox-mcp/internal/client/auth"
12+
"github.com/stackrox/stackrox-mcp/internal/logging"
1213
"github.com/stackrox/stackrox-mcp/internal/toolsets"
14+
"github.com/stackrox/stackrox-mcp/internal/util"
15+
)
16+
17+
const (
18+
defaultOffset = 0
19+
20+
// 0 = no limit.
21+
defaultLimit = 0
1322
)
1423

1524
// listClustersInput defines the input parameters for list_clusters tool.
16-
type listClustersInput struct{}
25+
type listClustersInput struct {
26+
Offset int `json:"offset,omitempty"`
27+
Limit int `json:"limit,omitempty"`
28+
}
29+
30+
// ClusterInfo represents information about a single cluster.
31+
type ClusterInfo struct {
32+
ID string `json:"id"`
33+
Name string `json:"name"`
34+
Type string `json:"type"`
35+
}
1736

1837
// listClustersOutput defines the output structure for list_clusters tool.
1938
type listClustersOutput struct {
20-
Clusters []string `json:"clusters"`
39+
Clusters []ClusterInfo `json:"clusters"`
40+
TotalCount int `json:"totalCount"`
41+
Offset int `json:"offset"`
42+
Limit int `json:"limit"`
2143
}
2244

2345
// listClustersTool implements the list_clusters tool.
@@ -48,63 +70,107 @@ func (t *listClustersTool) GetName() string {
4870
func (t *listClustersTool) GetTool() *mcp.Tool {
4971
return &mcp.Tool{
5072
Name: t.name,
51-
Description: "List all clusters managed by StackRox Central with their IDs, names, and types",
73+
Description: "List all clusters managed by StackRox with their IDs, names, and types",
74+
InputSchema: listClustersInputSchema(),
75+
}
76+
}
77+
78+
func listClustersInputSchema() *jsonschema.Schema {
79+
schema, err := jsonschema.For[listClustersInput](nil)
80+
if err != nil {
81+
logging.Fatal("Could not get jsonschema for list_clusters input", err)
82+
83+
return nil
5284
}
85+
86+
schema.Properties["offset"].Minimum = jsonschema.Ptr(0.0)
87+
schema.Properties["offset"].Default = util.MustMarshal(defaultOffset)
88+
schema.Properties["offset"].Description = "Starting index for pagination (0-based)"
89+
90+
schema.Properties["limit"].Minimum = jsonschema.Ptr(0.0)
91+
schema.Properties["limit"].Default = util.MustMarshal(defaultLimit)
92+
schema.Properties["limit"].Description = "Maximum number of clusters to return (default: 0 - unlimited)"
93+
94+
return schema
5395
}
5496

5597
// RegisterWith registers the list_clusters tool handler with the MCP server.
5698
func (t *listClustersTool) RegisterWith(server *mcp.Server) {
5799
mcp.AddTool(server, t.GetTool(), t.handle)
58100
}
59101

60-
// handle is the placeholder handler for list_clusters tool.
61-
func (t *listClustersTool) handle(
62-
ctx context.Context,
63-
req *mcp.CallToolRequest,
64-
_ listClustersInput,
65-
) (*mcp.CallToolResult, *listClustersOutput, error) {
102+
func (t *listClustersTool) getClusters(ctx context.Context, req *mcp.CallToolRequest) ([]ClusterInfo, error) {
66103
conn, err := t.client.ReadyConn(ctx)
67104
if err != nil {
68-
return nil, nil, errors.Wrap(err, "unable to connect to server")
105+
return nil, errors.Wrap(err, "unable to connect to server")
69106
}
70107

71108
callCtx := auth.WithMCPRequestContext(ctx, req)
72109

73110
// Create ClustersService client
74111
clustersClient := v1.NewClustersServiceClient(conn)
75112

76-
// Call GetClusters
113+
// Call GetClusters to fetch all clusters
77114
resp, err := clustersClient.GetClusters(callCtx, &v1.GetClustersRequest{})
78115
if err != nil {
79116
// Convert gRPC error to client error
80117
clientErr := client.NewError(err, "GetClusters")
81118

82-
return nil, nil, clientErr
119+
return nil, clientErr
83120
}
84121

85-
// Extract cluster information
86-
clusters := make([]string, 0, len(resp.GetClusters()))
122+
// Convert all clusters to ClusterInfo objects
123+
allClusters := make([]ClusterInfo, 0, len(resp.GetClusters()))
87124
for _, cluster := range resp.GetClusters() {
88-
// Format: "ID: <id>, Name: <name>, Type: <type>"
89-
clusterInfo := fmt.Sprintf("ID: %s, Name: %s, Type: %s",
90-
cluster.GetId(),
91-
cluster.GetName(),
92-
cluster.GetType().String())
93-
clusters = append(clusters, clusterInfo)
125+
clusterInfo := ClusterInfo{
126+
ID: cluster.GetId(),
127+
Name: cluster.GetName(),
128+
Type: cluster.GetType().String(),
129+
}
130+
allClusters = append(allClusters, clusterInfo)
94131
}
95132

96-
output := &listClustersOutput{
97-
Clusters: clusters,
133+
return allClusters, nil
134+
}
135+
136+
// handle is the handler for list_clusters tool.
137+
func (t *listClustersTool) handle(
138+
ctx context.Context,
139+
req *mcp.CallToolRequest,
140+
input listClustersInput,
141+
) (*mcp.CallToolResult, *listClustersOutput, error) {
142+
clusters, err := t.getClusters(ctx, req)
143+
if err != nil {
144+
return nil, nil, err
145+
}
146+
147+
totalCount := len(clusters)
148+
149+
// 0 = unlimited.
150+
limit := input.Limit
151+
if limit == 0 {
152+
limit = totalCount
153+
}
154+
155+
// Apply client-side pagination.
156+
var paginatedClusters []ClusterInfo
157+
if input.Offset >= totalCount {
158+
paginatedClusters = []ClusterInfo{}
159+
} else {
160+
end := min(input.Offset+limit, totalCount)
161+
if end < 0 {
162+
end = totalCount
163+
}
164+
165+
paginatedClusters = clusters[input.Offset:end]
98166
}
99167

100-
// Return result with text content
101-
result := &mcp.CallToolResult{
102-
Content: []mcp.Content{
103-
&mcp.TextContent{
104-
Text: fmt.Sprintf("Found %d cluster(s)", len(clusters)),
105-
},
106-
},
168+
output := &listClustersOutput{
169+
Clusters: paginatedClusters,
170+
TotalCount: totalCount,
171+
Offset: input.Offset,
172+
Limit: input.Limit,
107173
}
108174

109-
return result, output, nil
175+
return nil, output, nil
110176
}

0 commit comments

Comments
 (0)