Skip to content

Commit bd04eab

Browse files
authored
Add tool for creating cron jobs (#42)
Add `createCronJob` and `updateCronJob` tools.
1 parent aa1ad11 commit bd04eab

3 files changed

Lines changed: 431 additions & 1 deletion

File tree

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Get started with the MCP server by following the official docs: https://render.c
1111

1212
## Use Cases
1313

14-
- Creating and managing web services, static sites, and databases on Render
14+
- Creating and managing web services, static sites, cron jobs, and databases on Render
1515
- Monitoring application logs and deployment status to help troubleshoot issues
1616
- Monitoring service performance metrics for debugging, capacity planning, and optimization
1717
- Querying your Postgres databases directly inside an LLM
@@ -92,6 +92,44 @@ feature requests, bug reports, suggestions, comments, or concerns.
9292
- `publishPath`: Directory containing built assets (string, optional)
9393
- `envVars`: Environment variables array (array, optional)
9494

95+
- **create_cron_job** - Create a new cron job in your Render account
96+
97+
- `name`: A unique name for your cron job (string, required)
98+
- `schedule`: Cron schedule expression (string, required). Uses standard cron syntax with 5 fields: minute (0-59), hour (0-23), day of month (1-31), month (1-12), day of week (0-6, Sunday=0). Examples:
99+
- `0 0 * * *`: Daily at midnight
100+
- `*/15 * * * *`: Every 15 minutes
101+
- `0 9 * * 1-5`: Weekdays at 9am
102+
- `0 0 1 * *`: First day of each month at midnight
103+
- `runtime`: Runtime environment for your cron job (string, required). Accepted values:
104+
- `node`
105+
- `python`
106+
- `go`
107+
- `rust`
108+
- `ruby`
109+
- `elixir`
110+
- `docker`
111+
- `buildCommand`: Command used to build your cron job (string, required)
112+
- `startCommand`: Command that runs when your cron job executes (string, required)
113+
- `repo`: Repository containing source code (string, optional)
114+
- `branch`: Repository branch to deploy (string, optional)
115+
- `plan`: Plan for your cron job (string, optional). Accepted values:
116+
- `starter`
117+
- `standard`
118+
- `pro`
119+
- `pro_max`
120+
- `pro_plus`
121+
- `pro_ultra`
122+
- `autoDeploy`: Whether to automatically deploy the cron job (string, optional). Defaults to `yes`. Accepted values:
123+
- `yes`: Enable automatic deployments
124+
- `no`: Disable automatic deployments
125+
- `region`: Geographic region for deployment (string, optional). Defaults to `oregon`. Accepted values:
126+
- `oregon`
127+
- `frankfurt`
128+
- `singapore`
129+
- `ohio`
130+
- `virginia`
131+
- `envVars`: Environment variables array (array, optional)
132+
95133
- **update_environment_variables** - Update all environment variables for a service
96134
- `serviceId`: The ID of the service to update (string, required)
97135
- `envVars`: Complete list of environment variables (array, required)

pkg/service/tools.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ func Tools(c *client.ClientWithResponses) []server.ServerTool {
2222
getService(serviceRepo),
2323
createWebService(serviceRepo),
2424
createStaticSite(serviceRepo),
25+
createCronJob(serviceRepo),
2526
updateWebService(),
2627
updateStaticSite(),
28+
updateCronJob(),
2729
updateEnvVars(serviceRepo),
2830
}
2931
}
@@ -400,6 +402,186 @@ func createValidatedStaticSiteRequest(ctx context.Context, request mcp.CallToolR
400402
return validatedCreateServiceRequest(ctx, request, client.StaticSite, &serviceDetails)
401403
}
402404

405+
func createCronJob(serviceRepo *Repo) server.ServerTool {
406+
return server.ServerTool{
407+
Tool: mcp.NewTool("create_cron_job",
408+
mcp.WithDescription("Create a new cron job in your Render account. "+
409+
"A cron job is a scheduled task that runs on a recurring schedule specified using cron syntax. "+
410+
"Cron jobs are ideal for background tasks like data processing, cleanup operations, sending emails, or generating reports. "+
411+
"By default, these services are automatically deployed when the specified branch is updated. "+
412+
"This tool is currently limited to support only a subset of the cron job configuration parameters. "+
413+
"It also only supports cron jobs which don't use Docker, or a container registry. "+
414+
"To create a cron job without those limitations, please use the dashboard at: "+config.DashboardURL()+"/create"),
415+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
416+
Title: "Create cron job",
417+
ReadOnlyHint: pointers.From(false),
418+
IdempotentHint: pointers.From(false),
419+
OpenWorldHint: pointers.From(true),
420+
}),
421+
mcp.WithString("name",
422+
mcp.Required(),
423+
mcp.Description("A unique name for your cron job."),
424+
),
425+
mcp.WithString("schedule",
426+
mcp.Required(),
427+
mcp.Description("The cron schedule expression that determines when the job runs. "+
428+
"Uses standard cron syntax with 5 fields: minute (0-59), hour (0-23), day of month (1-31), month (1-12), day of week (0-6, Sunday=0). "+
429+
"Examples: '0 0 * * *' (daily at midnight), '*/15 * * * *' (every 15 minutes), '0 9 * * 1-5' (weekdays at 9am), '0 0 1 * *' (first day of each month at midnight). "+
430+
"For natural language requests like 'every hour' or 'daily at 3pm', convert to cron syntax."),
431+
),
432+
mcp.WithString("repo",
433+
mcp.Description("The repository containing the source code for your cron job. Must be a valid Git URL that Render can clone and deploy. Do not include the branch in the repo string. You can instead supply a 'branch' parameter."),
434+
),
435+
mcp.WithString("branch",
436+
mcp.Description("The repository branch to deploy. This branch will be deployed when you manually trigger deploys and when auto-deploy is enabled. If left empty, this will fall back to the default branch of the repository."),
437+
),
438+
mcp.WithString("autoDeploy",
439+
mcp.Description("Whether to automatically deploy the cron job when the specified branch is updated. Defaults to 'yes'."),
440+
mcp.Enum(string(client.AutoDeployYes), string(client.AutoDeployNo)),
441+
mcp.DefaultString(string(client.AutoDeployYes)),
442+
),
443+
mcp.WithString("runtime",
444+
mcp.Required(),
445+
mcp.Description("The runtime environment for your cron job. This determines how your job is built and run."),
446+
mcp.Enum("node", "python", "go", "rust", "ruby", "elixir", "docker"),
447+
),
448+
mcp.WithString("plan",
449+
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)),
452+
),
453+
mcp.WithString("buildCommand",
454+
mcp.Required(),
455+
mcp.Description("The command used to build your cron job. For example, 'npm install' for Node.js or 'pip install -r requirements.txt' for Python."),
456+
),
457+
mcp.WithString("startCommand",
458+
mcp.Required(),
459+
mcp.Description("The command that runs when your cron job executes. For example, 'node scripts/cleanup.js' for Node.js or 'python scripts/process_data.py' for Python."),
460+
),
461+
mcp.WithString("region",
462+
mcp.Description("The geographic region where your cron job will be deployed. Defaults to Oregon."),
463+
mcp.Enum(mcpserver.RegionEnumValues()...),
464+
mcp.DefaultString(string(client.Oregon)),
465+
),
466+
mcp.WithArray("envVars",
467+
mcp.Description("Environment variables to set for your cron job. These are exposed during builds and at runtime."),
468+
mcp.Items(
469+
map[string]interface{}{
470+
"type": "object",
471+
"additionalProperties": false,
472+
"required": []string{"key", "value"},
473+
"properties": map[string]interface{}{
474+
"key": map[string]interface{}{
475+
"type": "string",
476+
"description": "The name of the environment variable",
477+
},
478+
"value": map[string]interface{}{
479+
"type": "string",
480+
"description": "The value of the environment variable",
481+
},
482+
},
483+
},
484+
),
485+
),
486+
),
487+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
488+
requestBody, err := createValidatedCronJobRequest(ctx, request)
489+
if err != nil {
490+
return mcp.NewToolResultError(err.Error()), nil
491+
}
492+
493+
response, err := serviceRepo.CreateService(ctx, *requestBody)
494+
if err != nil {
495+
return mcp.NewToolResultError(err.Error()), nil
496+
}
497+
498+
respJSON, err := json.Marshal(response)
499+
if err != nil {
500+
return mcp.NewToolResultError(err.Error()), nil
501+
}
502+
503+
return mcp.NewToolResultText(string(respJSON)), nil
504+
},
505+
}
506+
}
507+
508+
func createValidatedCronJobRequest(ctx context.Context, request mcp.CallToolRequest) (*client.CreateServiceJSONRequestBody, error) {
509+
runtime, err := validate.RequiredToolParam[string](request, "runtime")
510+
if err != nil {
511+
return nil, err
512+
}
513+
514+
buildCommand, err := validate.RequiredToolParam[string](request, "buildCommand")
515+
if err != nil {
516+
return nil, err
517+
}
518+
519+
startCommand, err := validate.RequiredToolParam[string](request, "startCommand")
520+
if err != nil {
521+
return nil, err
522+
}
523+
524+
schedule, err := validate.RequiredToolParam[string](request, "schedule")
525+
if err != nil {
526+
return nil, err
527+
}
528+
529+
nativeEnvironmentDetails := client.NativeEnvironmentDetailsPOST{
530+
BuildCommand: buildCommand,
531+
StartCommand: startCommand,
532+
}
533+
534+
envSpecificDetailsPOST := client.EnvSpecificDetailsPOST{}
535+
if err = envSpecificDetailsPOST.FromNativeEnvironmentDetailsPOST(nativeEnvironmentDetails); err != nil {
536+
return nil, err
537+
}
538+
539+
// Convert EnvSpecificDetailsPOST to EnvSpecificDetails
540+
envSpecificDetails := client.EnvSpecificDetails{}
541+
nativeEnvDetails, err := envSpecificDetailsPOST.AsNativeEnvironmentDetailsPOST()
542+
if err != nil {
543+
return nil, err
544+
}
545+
// Convert POST type to regular type
546+
regularNativeEnvDetails := client.NativeEnvironmentDetails{
547+
BuildCommand: nativeEnvDetails.BuildCommand,
548+
StartCommand: nativeEnvDetails.StartCommand,
549+
PreDeployCommand: nil, // Not available in POST version
550+
}
551+
if err = envSpecificDetails.FromNativeEnvironmentDetails(regularNativeEnvDetails); err != nil {
552+
return nil, err
553+
}
554+
555+
cronJobDetailsPOST := client.CronJobDetailsPOST{
556+
Runtime: client.ServiceRuntime(runtime),
557+
Schedule: schedule,
558+
EnvSpecificDetails: &envSpecificDetails,
559+
}
560+
561+
if plan, ok, err := validate.OptionalToolParam[string](request, "plan"); err != nil {
562+
return nil, err
563+
} else if ok {
564+
paidPlan, err := validate.PaidPlan(plan)
565+
if err != nil {
566+
return nil, err
567+
}
568+
cronJobDetailsPOST.Plan = paidPlan
569+
}
570+
571+
if region, ok, err := validate.OptionalToolParam[string](request, "region"); err != nil {
572+
return nil, err
573+
} else if ok {
574+
cronJobDetailsPOST.Region = (*client.Region)(&region)
575+
}
576+
577+
serviceDetails := client.ServicePOST_ServiceDetails{}
578+
if err = serviceDetails.FromCronJobDetailsPOST(cronJobDetailsPOST); err != nil {
579+
return nil, err
580+
}
581+
582+
return validatedCreateServiceRequest(ctx, request, client.CronJob, &serviceDetails)
583+
}
584+
403585
func updateWebService() server.ServerTool {
404586
return server.ServerTool{
405587
Tool: mcp.NewTool("update_web_service",
@@ -456,6 +638,34 @@ func updateStaticSite() server.ServerTool {
456638
}
457639
}
458640

641+
func updateCronJob() server.ServerTool {
642+
return server.ServerTool{
643+
Tool: mcp.NewTool("update_cron_job",
644+
mcp.WithDescription("Update an existing cron job in your Render account."),
645+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
646+
Title: "Update cron job",
647+
ReadOnlyHint: pointers.From(true),
648+
IdempotentHint: pointers.From(true),
649+
}),
650+
mcp.WithString("serviceId",
651+
mcp.Required(),
652+
mcp.Description("The ID of the service to update"),
653+
),
654+
),
655+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
656+
serviceId, err := validate.RequiredToolParam[string](request, "serviceId")
657+
if err != nil {
658+
return mcp.NewToolResultError(err.Error()), nil
659+
}
660+
661+
// Return a message indicating direct updates are not supported via MCP server
662+
return mcp.NewToolResultText(
663+
"Updating a cron job directly is not supported. Please make changes using the dashboard or the API.\n\n" +
664+
"Dashboard URL: " + config.DashboardURL() + "/cron/" + serviceId + "/settings"), nil
665+
},
666+
}
667+
}
668+
459669
func updateEnvVars(serviceRepo *Repo) server.ServerTool {
460670
return server.ServerTool{
461671
Tool: mcp.NewTool("update_environment_variables",

0 commit comments

Comments
 (0)