/**
* Render namespace - Render deployment and monitoring
*/
import axios from 'axios';
import { MCPServer } from '../core/server.js';
import { ConfigMissingError, wrapError, RateLimitedError } from '../core/errors.js';
import {
RenderService,
RenderDeployment,
RenderServiceDetail,
DeploymentDetail,
ServiceMetrics,
LogEvent,
ScalePlan,
ListServicesResponse,
GetServiceResponse,
DeployResponse,
RedeployResponse,
ListDeploymentsResponse,
ServiceStatusResponse,
EnvListResponse,
LogsResponse,
ScaleResponse
} from '../types/render.js';
export class RenderNamespace {
private mcpServer: MCPServer;
private apiToken?: string;
private accountId?: string;
private baseUrl = 'https://api.render.com/v1';
constructor(mcpServer: MCPServer) {
this.mcpServer = mcpServer;
const env = mcpServer.getEnvConfig();
this.apiToken = env.RENDER_API_TOKEN;
this.accountId = env.RENDER_ACCOUNT_ID;
this.registerTools();
}
private checkAuth(): void {
if (!this.apiToken) {
throw new ConfigMissingError('RENDER_API_TOKEN');
}
}
private async makeRequest<T>(
method: string,
path: string,
data?: any,
params?: any
): Promise<T> {
this.checkAuth();
try {
const response = await axios({
method,
url: `${this.baseUrl}${path}`,
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
data,
params,
timeout: 60000
});
return response.data;
} catch (error: any) {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'];
throw new RateLimitedError(retryAfter ? parseInt(retryAfter) * 1000 : undefined);
}
throw wrapError(error);
}
}
private registerTools(): void {
const registry = this.mcpServer.getRegistry();
registry.registerTool(
'render.services_list',
{
name: 'render.services_list',
description: 'List Render services',
inputSchema: {
type: 'object',
properties: {
account_id: { type: 'string' }
}
}
},
this.servicesList.bind(this)
);
registry.registerTool(
'render.service_get',
{
name: 'render.service_get',
description: 'Get Render service details',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' }
},
required: ['service_id']
}
},
this.serviceGet.bind(this)
);
registry.registerTool(
'render.deploy',
{
name: 'render.deploy',
description: 'Deploy a Render service',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' },
source: {
type: 'object',
properties: {
image: { type: 'string' },
git_sha: { type: 'string' }
}
},
confirm: { type: 'boolean' },
request_id: { type: 'string' }
},
required: ['service_id', 'source']
}
},
this.deploy.bind(this)
);
registry.registerTool(
'render.redeploy',
{
name: 'render.redeploy',
description: 'Redeploy a Render service',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' },
from_deployment_id: { type: 'string' }
},
required: ['service_id']
}
},
this.redeploy.bind(this)
);
registry.registerTool(
'render.deployments_list',
{
name: 'render.deployments_list',
description: 'List deployments for a service',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' },
limit: { type: 'number' },
cursor: { type: 'string' }
},
required: ['service_id']
}
},
this.deploymentsList.bind(this)
);
registry.registerTool(
'render.deployment_get',
{
name: 'render.deployment_get',
description: 'Get deployment details',
inputSchema: {
type: 'object',
properties: {
deployment_id: { type: 'string' }
},
required: ['deployment_id']
}
},
this.deploymentGet.bind(this)
);
registry.registerTool(
'render.deployment_cancel',
{
name: 'render.deployment_cancel',
description: 'Cancel a deployment',
inputSchema: {
type: 'object',
properties: {
deployment_id: { type: 'string' }
},
required: ['deployment_id']
}
},
this.deploymentCancel.bind(this)
);
registry.registerTool(
'render.service_status',
{
name: 'render.service_status',
description: 'Get service status and metrics',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' }
},
required: ['service_id']
}
},
this.serviceStatus.bind(this)
);
registry.registerTool(
'render.env_list',
{
name: 'render.env_list',
description: 'List environment variables',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' }
},
required: ['service_id']
}
},
this.envList.bind(this)
);
registry.registerTool(
'render.env_set',
{
name: 'render.env_set',
description: 'Set environment variables',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' },
vars: { type: 'object' }
},
required: ['service_id', 'vars']
}
},
this.envSet.bind(this)
);
registry.registerTool(
'render.logs',
{
name: 'render.logs',
description: 'Get service logs',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' },
since: { type: 'string' },
tail: { type: 'number' },
level: { type: 'string', enum: ['info', 'warn', 'error'] }
},
required: ['service_id']
}
},
this.logs.bind(this)
);
registry.registerTool(
'render.scale',
{
name: 'render.scale',
description: 'Scale a service',
inputSchema: {
type: 'object',
properties: {
service_id: { type: 'string' },
plan: {
type: 'object',
properties: {
instances: { type: 'number' },
cpu: { type: 'string' },
mem: { type: 'string' }
}
}
},
required: ['service_id', 'plan']
}
},
this.scale.bind(this)
);
}
private async servicesList(params: {
account_id?: string;
}): Promise<ListServicesResponse> {
const accountId = params.account_id || this.accountId;
const response = await this.makeRequest<any[]>(
'GET',
'/services',
undefined,
accountId ? { ownerId: accountId } : undefined
);
const services: RenderService[] = response.map(s => ({
id: s.id,
name: s.name,
type: s.type,
region: s.region
}));
return { services };
}
private async serviceGet(params: {
service_id: string;
}): Promise<GetServiceResponse> {
const response = await this.makeRequest<any>(
'GET',
`/services/${params.service_id}`
);
return {
id: response.id,
name: response.name,
type: response.type,
region: response.region,
status: response.state,
last_deploy: response.lastDeploy ? {
id: response.lastDeploy.id,
created_at: response.lastDeploy.createdAt,
commit: response.lastDeploy.commit,
image: response.lastDeploy.image,
result: response.lastDeploy.status
} : undefined
};
}
private async deploy(params: {
service_id: string;
source: { image?: string; git_sha?: string };
confirm?: boolean;
request_id?: string;
}): Promise<DeployResponse> {
if (!params.confirm) {
// Dry run
return {
result: 'DRY',
revision: params.source.git_sha,
deployment_id: undefined,
url: undefined
};
}
const response = await this.makeRequest<any>(
'POST',
`/services/${params.service_id}/deploys`,
{
image: params.source.image,
commit: params.source.git_sha
}
);
return {
result: 'DEPLOYED',
revision: response.commit || params.source.git_sha,
deployment_id: response.id,
url: response.url
};
}
private async redeploy(params: {
service_id: string;
from_deployment_id?: string;
}): Promise<RedeployResponse> {
const deployData: any = {};
if (params.from_deployment_id) {
// Get the deployment details to copy
const deployment = await this.makeRequest<any>(
'GET',
`/deploys/${params.from_deployment_id}`
);
deployData.commit = deployment.commit;
deployData.image = deployment.image;
}
const response = await this.makeRequest<any>(
'POST',
`/services/${params.service_id}/deploys`,
deployData
);
return {
deployment_id: response.id,
status: response.status === 'build_in_progress' ? 'in_progress' : 'queued',
url: response.url
};
}
private async deploymentsList(params: {
service_id: string;
limit?: number;
cursor?: string;
}): Promise<ListDeploymentsResponse> {
const response = await this.makeRequest<any[]>(
'GET',
`/services/${params.service_id}/deploys`,
undefined,
{
limit: params.limit || 20,
cursor: params.cursor
}
);
const deployments: RenderDeployment[] = response.map(d => ({
id: d.id,
created_at: d.createdAt,
commit: d.commit,
image: d.image,
result: d.status,
url: d.url
}));
// Render API doesn't provide next_cursor in the same way
// We'll use the last deployment ID as cursor
const nextCursor = deployments.length === (params.limit || 20)
? deployments[deployments.length - 1].id
: undefined;
return {
deployments,
next_cursor: nextCursor
};
}
private async deploymentGet(params: {
deployment_id: string;
}): Promise<DeploymentDetail> {
const response = await this.makeRequest<any>(
'GET',
`/deploys/${params.deployment_id}`
);
let status: 'queued' | 'building' | 'deploying' | 'live' | 'failed' | 'cancelled';
switch (response.status) {
case 'created':
case 'pending':
status = 'queued';
break;
case 'build_in_progress':
status = 'building';
break;
case 'update_in_progress':
status = 'deploying';
break;
case 'live':
status = 'live';
break;
case 'build_failed':
case 'update_failed':
status = 'failed';
break;
case 'deactivated':
case 'canceled':
status = 'cancelled';
break;
default:
status = 'queued';
}
return {
id: response.id,
service_id: response.service.id,
status,
progress: response.buildInfo ? {
pct: response.buildInfo.progress || 0,
stage: response.buildInfo.stage || 'unknown'
} : undefined,
url: response.url
};
}
private async deploymentCancel(params: {
deployment_id: string;
}): Promise<{ ok: true }> {
await this.makeRequest<any>(
'DELETE',
`/deploys/${params.deployment_id}`
);
return { ok: true };
}
private async serviceStatus(params: {
service_id: string;
}): Promise<ServiceStatusResponse> {
const service = await this.serviceGet(params);
let status: 'ok' | 'degraded' | 'down';
switch (service.status) {
case 'live':
status = 'ok';
break;
case 'degraded':
status = 'degraded';
break;
case 'suspended':
case 'failed':
status = 'down';
break;
default:
status = 'degraded';
}
// Try to get metrics (may not be available for all service types)
let metrics: ServiceMetrics | undefined;
try {
const metricsResponse = await this.makeRequest<any>(
'GET',
`/services/${params.service_id}/metrics`
);
metrics = {
cpu: metricsResponse.cpu,
mem: metricsResponse.memory,
restarts: metricsResponse.restarts
};
} catch (error) {
// Metrics not available
}
return { status, metrics };
}
private async envList(params: {
service_id: string;
}): Promise<EnvListResponse> {
const response = await this.makeRequest<any[]>(
'GET',
`/services/${params.service_id}/env-vars`
);
const vars: Record<string, string> = {};
for (const envVar of response) {
vars[envVar.key] = envVar.value;
}
return { vars };
}
private async envSet(params: {
service_id: string;
vars: Record<string, string>;
}): Promise<{ ok: true }> {
const envVars = Object.entries(params.vars).map(([key, value]) => ({
key,
value
}));
await this.makeRequest<any>(
'PUT',
`/services/${params.service_id}/env-vars`,
envVars
);
return { ok: true };
}
private async logs(params: {
service_id: string;
since?: string;
tail?: number;
level?: 'info' | 'warn' | 'error';
}): Promise<LogsResponse> {
const response = await this.makeRequest<any[]>(
'GET',
`/services/${params.service_id}/logs`,
undefined,
{
since: params.since,
limit: params.tail || 100,
level: params.level
}
);
const events: LogEvent[] = response.map(log => ({
ts: log.timestamp,
level: log.level || 'info',
msg: log.message
}));
return { events };
}
private async scale(params: {
service_id: string;
plan: ScalePlan;
}): Promise<ScaleResponse> {
const updateData: any = {};
if (params.plan.instances !== undefined) {
updateData.numInstances = params.plan.instances;
}
if (params.plan.cpu || params.plan.mem) {
// Update service plan (would need to map to Render's plan names)
updateData.plan = this.mapToPlanName(params.plan.cpu, params.plan.mem);
}
const response = await this.makeRequest<any>(
'PATCH',
`/services/${params.service_id}`,
updateData
);
return {
ok: true,
current: {
instances: response.numInstances,
plan: response.plan
}
};
}
private mapToPlanName(cpu?: string, mem?: string): string {
// Map CPU/memory to Render plan names
// Service type mapping based on Render service characteristics
// to match Render's specific plan names
if (cpu === '0.5' && mem === '512MB') return 'starter';
if (cpu === '1' && mem === '2GB') return 'standard';
if (cpu === '2' && mem === '4GB') return 'pro';
if (cpu === '4' && mem === '8GB') return 'pro-plus';
return 'standard';
}
}