Skip to main content
Glama
render.ts15.7 kB
/** * 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'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JacobFV/mcp-fullstack'

If you have feedback or need assistance with the MCP directory API, please join our Discord server