Skip to content

Commit 3f20c73

Browse files
authored
Allow free plans for creating new services (#45)
1 parent 75ffabc commit 3f20c73

File tree

4 files changed

+110
-24
lines changed

4 files changed

+110
-24
lines changed

pkg/mcpserver/common.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import (
55
pgclient "github.com/render-oss/render-mcp-server/pkg/client/postgres"
66
)
77

8+
var ValidServicePlanValues = []client.PaidPlan{
9+
client.PaidPlan("free"),
10+
client.PaidPlanStarter,
11+
client.PaidPlanStandard,
12+
client.PaidPlanPro,
13+
client.PaidPlanProPlus,
14+
client.PaidPlanProMax,
15+
client.PaidPlanProUltra,
16+
}
17+
818
func RegionEnumValues() []string {
919
return EnumValuesFromClientType(
1020
client.Oregon,
@@ -15,6 +25,10 @@ func RegionEnumValues() []string {
1525
)
1626
}
1727

28+
func ServicePlanEnumValues() []string {
29+
return EnumValuesFromClientType(ValidServicePlanValues...)
30+
}
31+
1832
func PostgresPlanEnumValues() []string {
1933
return EnumValuesFromClientType(
2034
pgclient.Free,

pkg/service/tools.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ func createWebService(serviceRepo *Repo) server.ServerTool {
141141
),
142142
mcp.WithString("plan",
143143
mcp.Description("The pricing plan for your service. Different plans offer different levels of resources and features."),
144-
mcp.Enum(mcpserver.EnumValuesFromClientType(client.PaidPlanStarter, client.PaidPlanStandard, client.PaidPlanPro, client.PaidPlanProMax, client.PaidPlanProPlus, client.PaidPlanProUltra)...),
145-
mcp.DefaultString(string(client.PaidPlanStarter)),
144+
mcp.Enum(mcpserver.ServicePlanEnumValues()...),
145+
mcp.DefaultString(string(client.PlanFree)),
146146
),
147147
mcp.WithString("buildCommand",
148148
mcp.Required(),
@@ -233,11 +233,11 @@ func createValidatedWebServiceRequest(ctx context.Context, request mcp.CallToolR
233233
if plan, ok, err := validate.OptionalToolParam[string](request, "plan"); err != nil {
234234
return nil, err
235235
} else if ok {
236-
paidPlan, err := validate.PaidPlan(plan)
236+
servicePlan, err := validate.ServicePlan(plan)
237237
if err != nil {
238238
return nil, err
239239
}
240-
webServiceDetailsPOST.Plan = paidPlan
240+
webServiceDetailsPOST.Plan = servicePlan
241241
}
242242

243243
if region, ok, err := validate.OptionalToolParam[string](request, "region"); err != nil {
@@ -447,8 +447,8 @@ func createCronJob(serviceRepo *Repo) server.ServerTool {
447447
),
448448
mcp.WithString("plan",
449449
mcp.Description("The pricing plan for your cron job. Different plans offer different levels of resources and features."),
450-
mcp.Enum(mcpserver.EnumValuesFromClientType(client.PaidPlanStarter, client.PaidPlanStandard, client.PaidPlanPro, client.PaidPlanProMax, client.PaidPlanProPlus, client.PaidPlanProUltra)...),
451-
mcp.DefaultString(string(client.PaidPlanStarter)),
450+
mcp.Enum(mcpserver.ServicePlanEnumValues()...),
451+
mcp.DefaultString(string(client.PlanStarter)),
452452
),
453453
mcp.WithString("buildCommand",
454454
mcp.Required(),
@@ -561,11 +561,11 @@ func createValidatedCronJobRequest(ctx context.Context, request mcp.CallToolRequ
561561
if plan, ok, err := validate.OptionalToolParam[string](request, "plan"); err != nil {
562562
return nil, err
563563
} else if ok {
564-
paidPlan, err := validate.PaidPlan(plan)
564+
servicePlan, err := validate.ServicePlan(plan)
565565
if err != nil {
566566
return nil, err
567567
}
568-
cronJobDetailsPOST.Plan = paidPlan
568+
cronJobDetailsPOST.Plan = servicePlan
569569
}
570570

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

pkg/service/tools_test.go

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/render-oss/render-mcp-server/pkg/pointers"
1313
"github.com/render-oss/render-mcp-server/pkg/session"
1414
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
1516
)
1617

1718
func TestUpdateEnvVarsTool(t *testing.T) {
@@ -153,6 +154,80 @@ func envVarInputsAsParams(envVars []client.EnvVarInput) []interface{} {
153154
return envVarsAsParams
154155
}
155156

157+
func TestCreateWebServiceTool(t *testing.T) {
158+
ownerId := "own-123456"
159+
serviceName := "test-web-service"
160+
runtime := "node"
161+
buildCommand := "npm install"
162+
startCommand := "npm start"
163+
164+
tests := []struct {
165+
name string
166+
plan string
167+
expectedPlan *client.PaidPlan
168+
}{
169+
{
170+
name: "Create web service with free plan",
171+
plan: "free",
172+
expectedPlan: pointers.From(client.PaidPlan("free")),
173+
},
174+
{
175+
name: "Create web service with starter plan",
176+
plan: "starter",
177+
expectedPlan: pointers.From(client.PaidPlanStarter),
178+
},
179+
}
180+
181+
for _, tt := range tests {
182+
t.Run(tt.name, func(t *testing.T) {
183+
fakeClient := &fakes.FakeServiceRepoClient{}
184+
repo := NewRepo(fakeClient)
185+
186+
fakeClient.CreateServiceWithResponseReturns(&client.CreateServiceResponse{
187+
JSON201: &client.ServiceAndDeploy{
188+
Service: &client.Service{
189+
Id: "srv-web-123",
190+
Name: serviceName,
191+
Type: client.WebService,
192+
},
193+
},
194+
HTTPResponse: &http.Response{
195+
StatusCode: 201,
196+
},
197+
}, nil)
198+
199+
ctx := createTestContext(ownerId)
200+
201+
request := mcp.CallToolRequest{}
202+
request.Params.Arguments = map[string]any{
203+
"name": serviceName,
204+
"runtime": runtime,
205+
"buildCommand": buildCommand,
206+
"startCommand": startCommand,
207+
"plan": tt.plan,
208+
}
209+
210+
tool := createWebService(repo)
211+
result, err := tool.Handler(ctx, request)
212+
213+
require.NoError(t, err)
214+
require.NotNil(t, result)
215+
require.False(t, result.IsError, "expected no error but got: %v", result.Content)
216+
217+
assert.Equal(t, 1, fakeClient.CreateServiceWithResponseCallCount())
218+
_, requestBody, _ := fakeClient.CreateServiceWithResponseArgsForCall(0)
219+
assert.Equal(t, serviceName, requestBody.Name)
220+
assert.Equal(t, ownerId, requestBody.OwnerId)
221+
assert.Equal(t, client.WebService, requestBody.Type)
222+
223+
webServiceDetails, err := requestBody.ServiceDetails.AsWebServiceDetailsPOST()
224+
assert.NoError(t, err)
225+
assert.Equal(t, client.ServiceRuntime(runtime), webServiceDetails.Runtime)
226+
assert.Equal(t, tt.expectedPlan, webServiceDetails.Plan)
227+
})
228+
}
229+
}
230+
156231
func TestCreateCronJobTool(t *testing.T) {
157232
ownerId := "own-123456"
158233
cronJobName := "test-cron-job"
@@ -173,12 +248,12 @@ func TestCreateCronJobTool(t *testing.T) {
173248
}
174249

175250
tests := []struct {
176-
name string
177-
params map[string]interface{}
178-
expectedServiceType client.ServiceType
179-
expectedResponseCode int
180-
expectError bool
181-
validateRequestBody func(*testing.T, client.CreateServiceJSONRequestBody)
251+
name string
252+
params map[string]interface{}
253+
expectedServiceType client.ServiceType
254+
expectedResponseCode int
255+
expectError bool
256+
validateRequestBody func(*testing.T, client.CreateServiceJSONRequestBody)
182257
}{
183258
{
184259
name: "Create cron job with all required params",

pkg/validate/params.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package validate
33
import (
44
"errors"
55
"fmt"
6+
"slices"
67

78
"github.com/mark3labs/mcp-go/mcp"
89
"github.com/render-oss/render-mcp-server/pkg/client"
910
pgclient "github.com/render-oss/render-mcp-server/pkg/client/postgres"
1011
"github.com/render-oss/render-mcp-server/pkg/config"
12+
"github.com/render-oss/render-mcp-server/pkg/mcpserver"
1113
"github.com/render-oss/render-mcp-server/pkg/pointers"
1214
)
1315

@@ -126,17 +128,12 @@ func EnvVars(request mcp.CallToolRequest) ([]client.EnvVarInput, bool, error) {
126128
return envVars, true, nil
127129
}
128130

129-
func PaidPlan(plan string) (*client.PaidPlan, error) {
130-
switch client.PaidPlan(plan) {
131-
case client.PaidPlanStarter, client.PaidPlanStandard, client.PaidPlanPro,
132-
client.PaidPlanProMax, client.PaidPlanProPlus, client.PaidPlanProUltra:
133-
return pointers.From(client.PaidPlan(plan)), nil
134-
case "free":
135-
return nil, fmt.Errorf("MCP server doesn't support free plans. "+
136-
"If you're looking to create a free service, use the dashboard at: %s", config.DashboardURL())
137-
default:
138-
return nil, fmt.Errorf("invalid paid plan: %s", plan)
131+
func ServicePlan(plan string) (*client.PaidPlan, error) {
132+
paidPlan := client.PaidPlan(plan)
133+
if slices.Contains(mcpserver.ValidServicePlanValues, paidPlan) {
134+
return &paidPlan, nil
139135
}
136+
return nil, fmt.Errorf("invalid service plan: %s", plan)
140137
}
141138

142139
func KeyValuePlan(plan string) (*client.KeyValuePlan, error) {

0 commit comments

Comments
 (0)