Skip to content

Commit e5aa07c

Browse files
aaijaziclaude
authored andcommitted
Default Postgres and Key-Value services to free plan (#46)
- Make plan optional for Postgres and Key-Value creation, defaulting to free - Explicitly set free plan in handler when no plan specified - Add tests for schema defaults and handler behavior for all three service types (web services, postgres, key-value) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> GitOrigin-RevId: f4d7ff8
1 parent 1fe9879 commit e5aa07c

6 files changed

Lines changed: 236 additions & 21 deletions

File tree

pkg/keyvalue/tools.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ func createKeyValue(keyValueRepo *Repo) server.ServerTool {
105105
mcp.Description("Name of the Key Value instance"),
106106
),
107107
mcp.WithString("plan",
108-
mcp.Required(),
109108
mcp.Description("Pricing plan for the Key Value instance"),
110109
mcp.Enum(mcpserver.EnumValuesFromClientType(client.KeyValuePlanFree, client.KeyValuePlanStarter, client.KeyValuePlanStandard, client.KeyValuePlanPro, client.KeyValuePlanProPlus)...),
111110
mcp.DefaultString(string(client.KeyValuePlanFree)),
@@ -131,19 +130,23 @@ func createKeyValue(keyValueRepo *Repo) server.ServerTool {
131130
return mcp.NewToolResultError(err.Error()), nil
132131
}
133132

134-
plan, err := validate.RequiredToolParam[string](request, "plan")
135-
if err != nil {
136-
return mcp.NewToolResultError(err.Error()), nil
137-
}
138-
keyValuePlan, err := validate.KeyValuePlan(plan)
139-
if err != nil {
133+
var keyValuePlan client.KeyValuePlan
134+
if plan, ok, err := validate.OptionalToolParam[string](request, "plan"); err != nil {
140135
return mcp.NewToolResultError(err.Error()), nil
136+
} else if ok {
137+
kvPlan, err := validate.KeyValuePlan(plan)
138+
if err != nil {
139+
return mcp.NewToolResultError(err.Error()), nil
140+
}
141+
keyValuePlan = *kvPlan
142+
} else {
143+
keyValuePlan = client.KeyValuePlanFree
141144
}
142145

143146
createParams := client.KeyValuePOSTInput{
144147
Name: name,
145148
OwnerId: ownerId,
146-
Plan: *keyValuePlan,
149+
Plan: keyValuePlan,
147150
}
148151

149152
if region, ok, err := validate.OptionalToolParam[string](request, "region"); err != nil {

pkg/keyvalue/tools_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package keyvalue
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/render-oss/render-mcp-server/pkg/client"
10+
"github.com/render-oss/render-mcp-server/pkg/fakes"
11+
"github.com/render-oss/render-mcp-server/pkg/pointers"
12+
"github.com/render-oss/render-mcp-server/pkg/session"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestCreateKeyValueToolSchemaDefault(t *testing.T) {
18+
fakeClient := &fakes.FakeKeyValueRepoClient{}
19+
repo := NewRepo(fakeClient)
20+
tool := createKeyValue(repo)
21+
22+
planProp := tool.Tool.InputSchema.Properties["plan"].(map[string]any)
23+
assert.Equal(t, "free", planProp["default"])
24+
}
25+
26+
func TestCreateKeyValueTool(t *testing.T) {
27+
ownerId := "own-123456"
28+
kvName := "test-keyvalue"
29+
30+
tests := []struct {
31+
name string
32+
plan *string
33+
expectedPlan client.KeyValuePlan
34+
}{
35+
{
36+
name: "Create key value with no plan defaults to free",
37+
plan: nil,
38+
expectedPlan: client.KeyValuePlanFree,
39+
},
40+
{
41+
name: "Create key value with free plan",
42+
plan: pointers.From("free"),
43+
expectedPlan: client.KeyValuePlanFree,
44+
},
45+
{
46+
name: "Create key value with starter plan",
47+
plan: pointers.From("starter"),
48+
expectedPlan: client.KeyValuePlanStarter,
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
fakeClient := &fakes.FakeKeyValueRepoClient{}
55+
repo := NewRepo(fakeClient)
56+
57+
fakeClient.CreateKeyValueWithResponseReturns(&client.CreateKeyValueResponse{
58+
JSON201: &client.KeyValueDetail{
59+
Id: "kv-123",
60+
Name: kvName,
61+
},
62+
HTTPResponse: &http.Response{
63+
StatusCode: 201,
64+
},
65+
}, nil)
66+
67+
ctx := createTestContext(ownerId)
68+
69+
args := map[string]any{
70+
"name": kvName,
71+
}
72+
if tt.plan != nil {
73+
args["plan"] = *tt.plan
74+
}
75+
request := mcp.CallToolRequest{}
76+
request.Params.Arguments = args
77+
78+
tool := createKeyValue(repo)
79+
result, err := tool.Handler(ctx, request)
80+
81+
require.NoError(t, err)
82+
require.NotNil(t, result)
83+
require.False(t, result.IsError, "expected no error but got: %v", result.Content)
84+
85+
assert.Equal(t, 1, fakeClient.CreateKeyValueWithResponseCallCount())
86+
_, requestBody, _ := fakeClient.CreateKeyValueWithResponseArgsForCall(0)
87+
assert.Equal(t, kvName, requestBody.Name)
88+
assert.Equal(t, ownerId, requestBody.OwnerId)
89+
assert.Equal(t, tt.expectedPlan, requestBody.Plan)
90+
})
91+
}
92+
}
93+
94+
func createTestContext(workspaceID string) context.Context {
95+
ctx := session.ContextWithStdioSession(context.Background())
96+
sess := session.FromContext(ctx)
97+
sess.SetWorkspace(ctx, workspaceID)
98+
return ctx
99+
}

pkg/postgres/tools.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ func createPostgres(postgresRepo *Repo) server.ServerTool {
109109
mcp.Description("The name of the database as it will appear in the Render Dashboard"),
110110
),
111111
mcp.WithString("plan",
112-
mcp.Required(),
113112
mcp.Description("Pricing plan for the database"),
114113
mcp.Enum(mcpserver.PostgresPlanEnumValues()...),
114+
mcp.DefaultString(string(pgclient.Free)),
115115
),
116116
mcp.WithString("region",
117117
mcp.Description("Region where the database will be deployed"),
@@ -138,13 +138,16 @@ func createPostgres(postgresRepo *Repo) server.ServerTool {
138138
return mcp.NewToolResultError(err.Error()), nil
139139
}
140140

141-
plan, err := validate.RequiredToolParam[string](request, "plan")
142-
if err != nil {
143-
return mcp.NewToolResultError(err.Error()), nil
144-
}
145-
postgresPlan, err := validate.PostgresPlan(plan)
146-
if err != nil {
141+
var postgresPlan pgclient.PostgresPlans
142+
if plan, ok, err := validate.OptionalToolParam[string](request, "plan"); err != nil {
147143
return mcp.NewToolResultError(err.Error()), nil
144+
} else if ok {
145+
postgresPlan, err = validate.PostgresPlan(plan)
146+
if err != nil {
147+
return mcp.NewToolResultError(err.Error()), nil
148+
}
149+
} else {
150+
postgresPlan = pgclient.Free
148151
}
149152

150153
createParams := client.PostgresPOSTInput{

pkg/postgres/tools_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
"github.com/render-oss/render-mcp-server/pkg/client"
10+
pgclient "github.com/render-oss/render-mcp-server/pkg/client/postgres"
11+
"github.com/render-oss/render-mcp-server/pkg/fakes"
12+
"github.com/render-oss/render-mcp-server/pkg/pointers"
13+
"github.com/render-oss/render-mcp-server/pkg/session"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestCreatePostgresToolSchemaDefault(t *testing.T) {
19+
fakeClient := &fakes.FakePostgresRepoClient{}
20+
repo := NewRepo(fakeClient)
21+
tool := createPostgres(repo)
22+
23+
planProp := tool.Tool.InputSchema.Properties["plan"].(map[string]any)
24+
assert.Equal(t, "free", planProp["default"])
25+
}
26+
27+
func TestCreatePostgresTool(t *testing.T) {
28+
ownerId := "own-123456"
29+
dbName := "test-database"
30+
31+
tests := []struct {
32+
name string
33+
plan *string
34+
expectedPlan pgclient.PostgresPlans
35+
}{
36+
{
37+
name: "Create postgres with no plan defaults to free",
38+
plan: nil,
39+
expectedPlan: pgclient.Free,
40+
},
41+
{
42+
name: "Create postgres with free plan",
43+
plan: pointers.From("free"),
44+
expectedPlan: pgclient.Free,
45+
},
46+
{
47+
name: "Create postgres with basic_256mb plan",
48+
plan: pointers.From("basic_256mb"),
49+
expectedPlan: pgclient.Basic256mb,
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
fakeClient := &fakes.FakePostgresRepoClient{}
56+
repo := NewRepo(fakeClient)
57+
58+
fakeClient.CreatePostgresWithResponseReturns(&client.CreatePostgresResponse{
59+
JSON201: &client.PostgresDetail{
60+
Id: "pg-123",
61+
Name: dbName,
62+
},
63+
HTTPResponse: &http.Response{
64+
StatusCode: 201,
65+
},
66+
}, nil)
67+
68+
ctx := createTestContext(ownerId)
69+
70+
args := map[string]any{
71+
"name": dbName,
72+
}
73+
if tt.plan != nil {
74+
args["plan"] = *tt.plan
75+
}
76+
request := mcp.CallToolRequest{}
77+
request.Params.Arguments = args
78+
79+
tool := createPostgres(repo)
80+
result, err := tool.Handler(ctx, request)
81+
82+
require.NoError(t, err)
83+
require.NotNil(t, result)
84+
require.False(t, result.IsError, "expected no error but got: %v", result.Content)
85+
86+
assert.Equal(t, 1, fakeClient.CreatePostgresWithResponseCallCount())
87+
_, requestBody, _ := fakeClient.CreatePostgresWithResponseArgsForCall(0)
88+
assert.Equal(t, dbName, requestBody.Name)
89+
assert.Equal(t, ownerId, requestBody.OwnerId)
90+
assert.Equal(t, tt.expectedPlan, requestBody.Plan)
91+
})
92+
}
93+
}
94+
95+
func createTestContext(workspaceID string) context.Context {
96+
ctx := session.ContextWithStdioSession(context.Background())
97+
sess := session.FromContext(ctx)
98+
sess.SetWorkspace(ctx, workspaceID)
99+
return ctx
100+
}

pkg/service/tools.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ func createValidatedWebServiceRequest(ctx context.Context, request mcp.CallToolR
238238
return nil, err
239239
}
240240
webServiceDetailsPOST.Plan = servicePlan
241+
} else {
242+
webServiceDetailsPOST.Plan = pointers.From(client.PaidPlan("free"))
241243
}
242244

243245
if region, ok, err := validate.OptionalToolParam[string](request, "region"); err != nil {

pkg/service/tools_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,22 @@ func TestCreateWebServiceTool(t *testing.T) {
163163

164164
tests := []struct {
165165
name string
166-
plan string
166+
plan *string
167167
expectedPlan *client.PaidPlan
168168
}{
169+
{
170+
name: "Create web service with no plan defaults to free",
171+
plan: nil,
172+
expectedPlan: pointers.From(client.PaidPlan("free")),
173+
},
169174
{
170175
name: "Create web service with free plan",
171-
plan: "free",
176+
plan: pointers.From("free"),
172177
expectedPlan: pointers.From(client.PaidPlan("free")),
173178
},
174179
{
175180
name: "Create web service with starter plan",
176-
plan: "starter",
181+
plan: pointers.From("starter"),
177182
expectedPlan: pointers.From(client.PaidPlanStarter),
178183
},
179184
}
@@ -198,14 +203,17 @@ func TestCreateWebServiceTool(t *testing.T) {
198203

199204
ctx := createTestContext(ownerId)
200205

201-
request := mcp.CallToolRequest{}
202-
request.Params.Arguments = map[string]any{
206+
args := map[string]any{
203207
"name": serviceName,
204208
"runtime": runtime,
205209
"buildCommand": buildCommand,
206210
"startCommand": startCommand,
207-
"plan": tt.plan,
208211
}
212+
if tt.plan != nil {
213+
args["plan"] = *tt.plan
214+
}
215+
request := mcp.CallToolRequest{}
216+
request.Params.Arguments = args
209217

210218
tool := createWebService(repo)
211219
result, err := tool.Handler(ctx, request)

0 commit comments

Comments
 (0)