Skip to content

Commit 9dd59da

Browse files
authored
Merge pull request #42 from michaeldavidkelley/spaces-keys
spaces: keys tools
2 parents e4189c8 + 8b45062 commit 9dd59da

File tree

5 files changed

+909
-0
lines changed

5 files changed

+909
-0
lines changed

internal/registry.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"mcp-digitalocean/internal/common"
1111
"mcp-digitalocean/internal/droplet"
1212
"mcp-digitalocean/internal/networking"
13+
"mcp-digitalocean/internal/spaces"
1314

1415
"github.com/digitalocean/godo"
1516
"github.com/mark3labs/mcp-go/server"
@@ -21,6 +22,7 @@ var supportedServices = map[string]struct{}{
2122
"networking": {},
2223
"droplets": {},
2324
"accounts": {},
25+
"spaces": {},
2426
}
2527

2628
// registerAppTools registers the app platform tools with the MCP server.
@@ -149,6 +151,14 @@ func registerAccountTools(s *server.MCPServer, c *godo.Client) error {
149151
return nil
150152
}
151153

154+
// registerSpacesTools registers the spaces tools and resources with the MCP server.
155+
func registerSpacesTools(s *server.MCPServer, c *godo.Client) error {
156+
// Register the tools for spaces keys
157+
s.AddTools(spaces.NewSpacesKeysTool(c).Tools()...)
158+
159+
return nil
160+
}
161+
152162
// Register registers the set of tools for the specified services with the MCP server.
153163
// We either register a subset of tools of the services are specified, or we register all tools if no services are specified.
154164
func Register(logger *slog.Logger, s *server.MCPServer, c *godo.Client, servicesToActivate ...string) error {
@@ -177,6 +187,10 @@ func Register(logger *slog.Logger, s *server.MCPServer, c *godo.Client, services
177187
if err := registerAccountTools(s, c); err != nil {
178188
return fmt.Errorf("failed to register account tools: %w", err)
179189
}
190+
case "spaces":
191+
if err := registerSpacesTools(s, c); err != nil {
192+
return fmt.Errorf("failed to register spaces tools: %w", err)
193+
}
180194
default:
181195
return fmt.Errorf("unsupported service: %s, supported service are: %v", svc, setToString(supportedServices))
182196
}

internal/spaces/generate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package spaces
2+
3+
//go:generate mockgen -destination=./mocks.go -package spaces github.com/digitalocean/godo SpacesKeysService

internal/spaces/keys_tools.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package spaces
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/digitalocean/godo"
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/mark3labs/mcp-go/server"
11+
)
12+
13+
type KeysTool struct {
14+
client *godo.Client
15+
}
16+
17+
func NewSpacesKeysTool(client *godo.Client) *KeysTool {
18+
return &KeysTool{
19+
client: client,
20+
}
21+
}
22+
23+
func (s *KeysTool) createSpacesKey(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
24+
args := req.GetArguments()
25+
nameArg, ok := args["Name"]
26+
if !ok {
27+
return mcp.NewToolResultError("Name parameter is required"), nil
28+
}
29+
30+
name, ok := nameArg.(string)
31+
if !ok {
32+
return mcp.NewToolResultError("Name must be a string"), nil
33+
}
34+
35+
if name == "" {
36+
return mcp.NewToolResultError("Name cannot be empty"), nil
37+
}
38+
39+
createRequest := &godo.SpacesKeyCreateRequest{
40+
Name: name,
41+
Grants: []*godo.Grant{
42+
{
43+
Bucket: "",
44+
Permission: godo.SpacesKeyFullAccess,
45+
},
46+
},
47+
}
48+
49+
key, _, err := s.client.SpacesKeys.Create(ctx, createRequest)
50+
if err != nil {
51+
return mcp.NewToolResultError(err.Error()), nil
52+
}
53+
54+
jsonKey, err := json.MarshalIndent(key, "", " ")
55+
if err != nil {
56+
return nil, fmt.Errorf("marshal error: %w", err)
57+
}
58+
59+
return mcp.NewToolResultText(string(jsonKey)), nil
60+
}
61+
62+
func (s *KeysTool) updateSpacesKey(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
63+
args := req.GetArguments()
64+
65+
accessKeyArg, ok := args["AccessKey"]
66+
if !ok {
67+
return mcp.NewToolResultError("AccessKey parameter is required"), nil
68+
}
69+
70+
accessKey, ok := accessKeyArg.(string)
71+
if !ok {
72+
return mcp.NewToolResultError("AccessKey must be a string"), nil
73+
}
74+
75+
if accessKey == "" {
76+
return mcp.NewToolResultError("AccessKey cannot be empty"), nil
77+
}
78+
79+
nameArg, ok := args["Name"]
80+
if !ok {
81+
return mcp.NewToolResultError("Name parameter is required"), nil
82+
}
83+
84+
name, ok := nameArg.(string)
85+
if !ok {
86+
return mcp.NewToolResultError("Name must be a string"), nil
87+
}
88+
89+
if name == "" {
90+
return mcp.NewToolResultError("Name cannot be empty"), nil
91+
}
92+
93+
updateRequest := &godo.SpacesKeyUpdateRequest{
94+
Name: name,
95+
}
96+
97+
key, _, err := s.client.SpacesKeys.Update(ctx, accessKey, updateRequest)
98+
if err != nil {
99+
return mcp.NewToolResultError(err.Error()), nil
100+
}
101+
102+
jsonKey, err := json.MarshalIndent(key, "", " ")
103+
if err != nil {
104+
return nil, fmt.Errorf("marshal error: %w", err)
105+
}
106+
107+
return mcp.NewToolResultText(string(jsonKey)), nil
108+
}
109+
110+
func (s *KeysTool) deleteSpacesKey(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
111+
args := req.GetArguments()
112+
113+
accessKeyArg, ok := args["AccessKey"]
114+
if !ok {
115+
return mcp.NewToolResultError("AccessKey parameter is required"), nil
116+
}
117+
118+
accessKey, ok := accessKeyArg.(string)
119+
if !ok {
120+
return mcp.NewToolResultError("AccessKey must be a string"), nil
121+
}
122+
123+
if accessKey == "" {
124+
return mcp.NewToolResultError("AccessKey cannot be empty"), nil
125+
}
126+
127+
_, err := s.client.SpacesKeys.Delete(ctx, accessKey)
128+
if err != nil {
129+
return mcp.NewToolResultError(err.Error()), nil
130+
}
131+
132+
return mcp.NewToolResultText("Spaces key deleted successfully"), nil
133+
}
134+
135+
func (s *KeysTool) listSpacesKeys(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
136+
args := req.GetArguments()
137+
138+
// Set up pagination options with defaults
139+
listOpts := &godo.ListOptions{
140+
Page: 1, // Default to page 1
141+
PerPage: 10, // Default to 10 per page
142+
}
143+
144+
// Handle Page parameter
145+
if pageRaw, ok := args["Page"]; ok {
146+
if pageFloat, ok := pageRaw.(float64); ok {
147+
listOpts.Page = int(pageFloat)
148+
} else {
149+
return mcp.NewToolResultError("Page must be a number"), nil
150+
}
151+
}
152+
153+
// Handle PerPage parameter
154+
if perPageRaw, ok := args["PerPage"]; ok {
155+
if perPageFloat, ok := perPageRaw.(float64); ok {
156+
listOpts.PerPage = int(perPageFloat)
157+
} else {
158+
return mcp.NewToolResultError("PerPage must be a number"), nil
159+
}
160+
}
161+
162+
keys, resp, err := s.client.SpacesKeys.List(ctx, listOpts)
163+
if err != nil {
164+
return mcp.NewToolResultError(err.Error()), nil
165+
}
166+
167+
// Create response with pagination info
168+
result := struct {
169+
Keys []*godo.SpacesKey `json:"keys"`
170+
Meta *godo.Meta `json:"meta,omitempty"`
171+
}{
172+
Keys: keys,
173+
Meta: resp.Meta,
174+
}
175+
176+
jsonKeys, err := json.MarshalIndent(result, "", " ")
177+
if err != nil {
178+
return nil, fmt.Errorf("marshal error: %w", err)
179+
}
180+
181+
return mcp.NewToolResultText(string(jsonKeys)), nil
182+
}
183+
184+
func (s *KeysTool) getSpacesKey(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
185+
args := req.GetArguments()
186+
187+
accessKeyArg, ok := args["AccessKey"]
188+
if !ok {
189+
return mcp.NewToolResultError("AccessKey parameter is required"), nil
190+
}
191+
192+
accessKey, ok := accessKeyArg.(string)
193+
if !ok {
194+
return mcp.NewToolResultError("AccessKey must be a string"), nil
195+
}
196+
197+
if accessKey == "" {
198+
return mcp.NewToolResultError("AccessKey cannot be empty"), nil
199+
}
200+
201+
key, _, err := s.client.SpacesKeys.Get(ctx, accessKey)
202+
if err != nil {
203+
return mcp.NewToolResultError(err.Error()), nil
204+
}
205+
206+
jsonKey, err := json.MarshalIndent(key, "", " ")
207+
if err != nil {
208+
return nil, fmt.Errorf("marshal error: %w", err)
209+
}
210+
211+
return mcp.NewToolResultText(string(jsonKey)), nil
212+
}
213+
214+
func (s *KeysTool) Tools() []server.ServerTool {
215+
return []server.ServerTool{
216+
{
217+
Handler: s.listSpacesKeys,
218+
Tool: mcp.NewTool("digitalocean-spaces-key-list",
219+
mcp.WithDescription("List all Spaces keys"),
220+
mcp.WithNumber("Page", mcp.Required(), mcp.DefaultNumber(1), mcp.Description("Page number for pagination")),
221+
mcp.WithNumber("PerPage", mcp.Required(), mcp.DefaultNumber(10), mcp.Description("Number of items per page"), mcp.Max(100)),
222+
),
223+
},
224+
{
225+
Handler: s.getSpacesKey,
226+
Tool: mcp.NewTool("digitalocean-spaces-key-get",
227+
mcp.WithDescription("Get a specific Spaces key"),
228+
mcp.WithString("AccessKey", mcp.Required(), mcp.Description("Access Key of the Spaces key to retrieve")),
229+
),
230+
},
231+
{
232+
Handler: s.createSpacesKey,
233+
Tool: mcp.NewTool("digitalocean-spaces-key-create",
234+
mcp.WithDescription("Create a new Spaces key"),
235+
mcp.WithString("Name", mcp.Required(), mcp.Description("Name for the Spaces key")),
236+
),
237+
},
238+
{
239+
Handler: s.updateSpacesKey,
240+
Tool: mcp.NewTool("digitalocean-spaces-key-update",
241+
mcp.WithDescription("Update an existing Spaces key"),
242+
mcp.WithString("AccessKey", mcp.Required(), mcp.Description("Access Key of the Spaces key to update")),
243+
mcp.WithString("Name", mcp.Required(), mcp.Description("New name for the Spaces key")),
244+
),
245+
},
246+
{
247+
Handler: s.deleteSpacesKey,
248+
Tool: mcp.NewTool("digitalocean-spaces-key-delete",
249+
mcp.WithDescription("Delete a Spaces key"),
250+
mcp.WithString("AccessKey", mcp.Required(), mcp.Description("Access Key of the Spaces key to delete")),
251+
),
252+
},
253+
}
254+
}

0 commit comments

Comments
 (0)