Skip to content

Commit 240cb9c

Browse files
feat(networking): add vpc-peering tools and resources (#43)
1 parent 9dd59da commit 240cb9c

File tree

6 files changed

+413
-2
lines changed

6 files changed

+413
-2
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Each service provides a toolset to interact with DigitalOcean.
7575
| **Account** | `digitalocean-key-create`, `digitalocean-key-delete`, `account://current`, `balance://current`, `billing://{last}`, `invoice://{last}`, `actions://{id}`, `keys://{id}` |
7676
| **Apps** | `digitalocean-create-app-from-spec`, `digitalocean-apps-update`, `digitalocean-apps-delete`, `digitalocean-apps-get-info`, `digitalocean-apps-usage`, `digitalocean-apps-get-deployment-status`, `digitalocean-apps-list` |
7777
| **Droplets** | `digitalocean-droplet-create`, `digitalocean-droplet-delete`, `digitalocean-droplet-power-cycle`, `digitalocean-droplet-resize`, `digitalocean-droplet-snapshot`, `digitalocean-droplet-enable-backups`, `digitalocean-droplet-get-neighbors`, `digitalocean-droplet-rename`, `digitalocean-droplet-rebuild`, `digitalocean-droplet-get-kernels`, ... (see [Droplet README](./internal/droplet/README.md)) |
78-
| **Networking** | `digitalocean-domain-create`, `digitalocean-domain-delete`, `digitalocean-domain-record-create`, `digitalocean-domain-record-delete`, `digitalocean-certificate-create`, `digitalocean-certificate-delete`, `digitalocean-firewall-create`, `digitalocean-firewall-delete`, `digitalocean-reserved-ip-reserve`, `digitalocean-reserved-ip-release`, `digitalocean-vpc-create`, `digitalocean-vpc-delete`, `digitalocean-cdn-create`, `digitalocean-cdn-delete`, `digitalocean-partner-attachment-create`, ... (see [Networking README](./internal/networking/README.md)) |
78+
| **Networking** | `digitalocean-domain-create`, `digitalocean-domain-delete`, `digitalocean-domain-record-create`, `digitalocean-domain-record-delete`, `digitalocean-certificate-create`, `digitalocean-certificate-delete`, `digitalocean-firewall-create`, `digitalocean-firewall-delete`, `digitalocean-reserved-ip-reserve`, `digitalocean-reserved-ip-release`, `digitalocean-vpc-create`, `digitalocean-vpc-delete`, `digitalocean-vpc-peering-create`, `digitalocean-vpc-peering-delete`, `digitalocean-cdn-create`, `digitalocean-cdn-delete`, `digitalocean-partner-attachment-create`, ... (see [Networking README](./internal/networking/README.md)) |
7979

8080
---
8181
## Service Documentation
@@ -97,7 +97,7 @@ Each service exposes resources that can be queried directly. Examples:
9797
- **Account:** `account://current`, `balance://current`, `billing://3`, `invoice://6`, `actions://123456`, `keys://987654`
9898
- **Apps:** `apps://{id}`, `apps://{id}/deployments/{deployment_id}`
9999
- **Droplets:** `droplets://{id}`, `droplets://{id}/actions/{action_id}`, `images://distribution`, `images://{id}`, `sizes://all`
100-
- **Networking:** `domains://{name}`, `domains://{name}/records/{record_id}`, `certificates://{id}`, `firewalls://{id}`, `reserved_ipv4://{ip}`, `vpcs://{id}`, `cdn://{id}`, `partner_attachment://{id}`
100+
- **Networking:** `domains://{name}`, `domains://{name}/records/{record_id}`, `certificates://{id}`, `firewalls://{id}`, `reserved_ipv4://{ip}`, `vpcs://{id}`, `cdn://{id}`, `partner_attachment://{id}`, `vpc_peering://{id}`
101101

102102
---
103103

@@ -111,6 +111,8 @@ Each service exposes resources that can be queried directly. Examples:
111111
- Flush a CDN cache: `digitalocean-cdn-flush-cache`
112112
- List all available droplet sizes: `sizes://all`
113113
- Get account balance: `balance://current`
114+
- Create a VPC peering connection: `digitalocean-vpc-peering-create`
115+
- Delete a VPC peering connection: `digitalocean-vpc-peering-delete`
114116

115117
---
116118

internal/networking/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ This directory contains tools and resources for managing DigitalOcean networking
120120

121121
---
122122

123+
### VPC Peering
124+
125+
- **`digitalocean-vpc-peering-create`**
126+
Create a new VPC Peering connection between two VPCs.
127+
- `Name` (string, required): Name for the Peering connection
128+
- `Vpc1` (string, required): ID of the first VPC
129+
- `Vpc2` (string, required): ID of the second VPC
130+
131+
- **`digitalocean-vpc-peering-delete`**
132+
Delete a VPC Peering connection.
133+
- `ID` (string, required): ID of the VPC Peering connection to delete
134+
135+
---
136+
123137
### VPCs
124138

125139
- **`digitalocean-vpc-create`**
@@ -156,6 +170,10 @@ This directory contains tools and resources for managing DigitalOcean networking
156170

157171
---
158172

173+
- **`vpc_peering://{id}`**
174+
Returns information about a specific VPC Peering connection.
175+
176+
159177
### Partner Attachments
160178

161179
- **`digitalocean-partner-attachment-create`**
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package networking
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"mcp-digitalocean/internal/common"
8+
9+
"github.com/digitalocean/godo"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
const VPCPeeringURI = "vpc_peering://"
15+
16+
type VPCPeeringMCPResource struct {
17+
client *godo.Client
18+
}
19+
20+
func NewVPCPeeringMCPResource(client *godo.Client) *VPCPeeringMCPResource {
21+
return &VPCPeeringMCPResource{
22+
client: client,
23+
}
24+
}
25+
26+
func (v *VPCPeeringMCPResource) getVPCPeeringResourceTemplate() mcp.ResourceTemplate {
27+
return mcp.NewResourceTemplate(
28+
VPCPeeringURI+"{id}",
29+
"VPC Peering",
30+
mcp.WithTemplateDescription("Returns vpc peering information about a given peering id"),
31+
mcp.WithTemplateMIMEType("application/json"),
32+
)
33+
}
34+
35+
func (v *VPCPeeringMCPResource) handleGetVPCPeering(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
36+
peeringID, err := common.ExtractStringIDFromURI(req.Params.URI)
37+
if err != nil {
38+
return nil, fmt.Errorf("invalid vpc peering URI: %w", err)
39+
}
40+
41+
vpcPeering, _, err := v.client.VPCs.GetVPCPeering(ctx, peeringID)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to get vpc peering: %w", err)
44+
}
45+
46+
jsonData, err := json.MarshalIndent(vpcPeering, "", " ")
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to marshal vpc peering: %w", err)
49+
}
50+
51+
return []mcp.ResourceContents{
52+
mcp.TextResourceContents{
53+
URI: req.Params.URI,
54+
MIMEType: "application/json",
55+
Text: string(jsonData),
56+
},
57+
}, nil
58+
}
59+
60+
func (v *VPCPeeringMCPResource) ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc {
61+
return map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc{
62+
v.getVPCPeeringResourceTemplate(): v.handleGetVPCPeering,
63+
}
64+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package networking
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"testing"
8+
9+
"github.com/digitalocean/godo"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/stretchr/testify/require"
12+
"go.uber.org/mock/gomock"
13+
)
14+
15+
func setupVPCPeeringResourceWithMock(vpcs *MockVPCsService) *VPCPeeringMCPResource {
16+
client := &godo.Client{}
17+
client.VPCs = vpcs
18+
return NewVPCPeeringMCPResource(client)
19+
}
20+
21+
func TestVPCPeeringMCPResource_handleGetVPCPeering(t *testing.T) {
22+
ctrl := gomock.NewController(t)
23+
defer ctrl.Finish()
24+
25+
testPeering := &godo.VPCPeering{
26+
ID: "peer-123",
27+
Name: "test-peering",
28+
}
29+
tests := []struct {
30+
name string
31+
uri string
32+
mockSetup func(*MockVPCsService)
33+
expectError bool
34+
}{
35+
{
36+
name: "Successful get",
37+
uri: "vpc_peering://peer-123",
38+
mockSetup: func(m *MockVPCsService) {
39+
m.EXPECT().
40+
GetVPCPeering(gomock.Any(), "peer-123").
41+
Return(testPeering, nil, nil).
42+
Times(1)
43+
},
44+
},
45+
{
46+
name: "API error",
47+
uri: "vpc_peering://peer-456",
48+
mockSetup: func(m *MockVPCsService) {
49+
m.EXPECT().
50+
GetVPCPeering(gomock.Any(), "peer-456").
51+
Return(nil, nil, errors.New("api error")).
52+
Times(1)
53+
},
54+
expectError: true,
55+
},
56+
{
57+
name: "Invalid URI",
58+
uri: "vpc_peering-peer-789",
59+
mockSetup: nil,
60+
expectError: true,
61+
},
62+
}
63+
64+
for _, tc := range tests {
65+
t.Run(tc.name, func(t *testing.T) {
66+
mockVPC := NewMockVPCsService(ctrl)
67+
if tc.mockSetup != nil {
68+
tc.mockSetup(mockVPC)
69+
}
70+
resource := setupVPCPeeringResourceWithMock(mockVPC)
71+
req := mcp.ReadResourceRequest{
72+
Params: mcp.ReadResourceParams{
73+
URI: tc.uri,
74+
},
75+
}
76+
resp, err := resource.handleGetVPCPeering(context.Background(), req)
77+
if tc.expectError {
78+
require.Error(t, err)
79+
require.Nil(t, resp)
80+
return
81+
}
82+
require.NoError(t, err)
83+
require.NotNil(t, resp)
84+
require.Len(t, resp, 1)
85+
content, ok := resp[0].(mcp.TextResourceContents)
86+
require.True(t, ok)
87+
require.Equal(t, tc.uri, content.URI)
88+
require.Equal(t, "application/json", content.MIMEType)
89+
var outPeering godo.VPCPeering
90+
require.NoError(t, json.Unmarshal([]byte(content.Text), &outPeering))
91+
require.Equal(t, testPeering.ID, outPeering.ID)
92+
})
93+
}
94+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package networking
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+
// VPCPeeringTool represents a tool for managing VPC peering connections.
14+
type VPCPeeringTool struct {
15+
client *godo.Client
16+
}
17+
18+
// NewVPCPeeringTool creates a new VPCPeeringTool instance.
19+
func NewVPCPeeringTool(client *godo.Client) *VPCPeeringTool {
20+
return &VPCPeeringTool{
21+
client: client,
22+
}
23+
}
24+
25+
func (t *VPCPeeringTool) createPeering(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
26+
args := req.GetArguments()
27+
28+
peeringName := args["Name"].(string)
29+
vpc1 := args["Vpc1"].(string)
30+
vpc2 := args["Vpc2"].(string)
31+
32+
// Create a new VPC peering connection
33+
peering, _, err := t.client.VPCs.CreateVPCPeering(ctx, &godo.VPCPeeringCreateRequest{
34+
Name: peeringName,
35+
VPCIDs: []string{vpc1, vpc2},
36+
})
37+
if err != nil {
38+
return mcp.NewToolResultErrorFromErr("api error", err), nil
39+
}
40+
41+
jsonData, err := json.MarshalIndent(peering, "", " ")
42+
if err != nil {
43+
return nil, fmt.Errorf("marshal error: %w", err)
44+
}
45+
46+
return mcp.NewToolResultText(string(jsonData)), nil
47+
}
48+
49+
func (t *VPCPeeringTool) deletePeering(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
50+
args := req.GetArguments()
51+
52+
peeringID := args["ID"].(string)
53+
54+
// Delete the VPC peering connection
55+
_, err := t.client.VPCs.DeleteVPCPeering(ctx, peeringID)
56+
if err != nil {
57+
return mcp.NewToolResultErrorFromErr("api error", err), nil
58+
}
59+
60+
return mcp.NewToolResultText("VPC peering connection deleted"), nil
61+
}
62+
63+
func (t *VPCPeeringTool) Tools() []server.ServerTool {
64+
return []server.ServerTool{
65+
{
66+
Handler: t.createPeering,
67+
Tool: mcp.NewTool("digitalocean-vpc-peering-create",
68+
mcp.WithDescription("Create a new VPC Peering connection between two VPCs"),
69+
mcp.WithString("Name", mcp.Required(), mcp.Description("Name for the Peering connection")),
70+
mcp.WithString("Vpc1", mcp.Required(), mcp.Description("ID of the first VPC")),
71+
mcp.WithString("Vpc2", mcp.Required(), mcp.Description("ID of the second VPC")),
72+
),
73+
},
74+
{
75+
Handler: t.deletePeering,
76+
Tool: mcp.NewTool("digitalocean-vpc-peering-delete",
77+
mcp.WithDescription("Delete a VPC Peering connection"),
78+
mcp.WithString("ID", mcp.Required(), mcp.Description("ID of the VPC Peering connection to delete")),
79+
),
80+
},
81+
}
82+
}

0 commit comments

Comments
 (0)