/**
* Vercel namespace - Vercel deployment and domain management
*/
import axios from 'axios';
import { MCPServer } from '../core/server.js';
import { ConfigMissingError, wrapError, RateLimitedError } from '../core/errors.js';
import {
VercelProject,
VercelProjectDetail,
VercelDeployment,
VercelDeploymentDetail,
VercelLogLine,
VercelDomain,
DeploymentTarget,
ListProjectsResponse,
GetProjectResponse,
DeployResponse,
RedeployResponse,
ListDeploymentsResponse,
GetDeploymentResponse,
PromoteResponse,
LogsResponse,
EnvListResponse,
EnvSetResponse,
ListDomainsResponse,
AddDomainResponse,
RemoveDomainResponse
} from '../types/vercel.js';
export class VercelNamespace {
private mcpServer: MCPServer;
private apiToken?: string;
private teamId?: string;
private baseUrl = 'https://api.vercel.com';
constructor(mcpServer: MCPServer) {
this.mcpServer = mcpServer;
const env = mcpServer.getEnvConfig();
this.apiToken = env.VERCEL_TOKEN;
this.teamId = env.VERCEL_TEAM_ID;
this.registerTools();
}
private checkAuth(): void {
if (!this.apiToken) {
throw new ConfigMissingError('VERCEL_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'
},
data,
params: {
...params,
...(this.teamId ? { teamId: this.teamId } : {})
},
timeout: 60000
});
return response.data;
} catch (error: any) {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['x-ratelimit-reset'];
const retryMs = retryAfter ?
Math.max(0, parseInt(retryAfter) * 1000 - Date.now()) :
undefined;
throw new RateLimitedError(retryMs);
}
throw wrapError(error);
}
}
private registerTools(): void {
const registry = this.mcpServer.getRegistry();
registry.registerTool(
'vercel.projects_list',
{
name: 'vercel.projects_list',
description: 'List Vercel projects',
inputSchema: {
type: 'object',
properties: {
team_id: { type: 'string' }
}
}
},
this.projectsList.bind(this)
);
registry.registerTool(
'vercel.project_get',
{
name: 'vercel.project_get',
description: 'Get Vercel project details',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' }
},
required: ['project_id']
}
},
this.projectGet.bind(this)
);
registry.registerTool(
'vercel.deploy',
{
name: 'vercel.deploy',
description: 'Deploy to Vercel',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' },
source: {
type: 'object',
properties: {
git_sha: { type: 'string' },
build_id: { type: 'string' }
}
},
target: { type: 'string', enum: ['production', 'preview'] }
},
required: ['project_id', 'source']
}
},
this.deploy.bind(this)
);
registry.registerTool(
'vercel.redeploy',
{
name: 'vercel.redeploy',
description: 'Redeploy a Vercel deployment',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' },
from_deployment_id: { type: 'string' },
from_git_sha: { type: 'string' },
target: { type: 'string', enum: ['production', 'preview'] }
},
required: ['project_id']
}
},
this.redeploy.bind(this)
);
registry.registerTool(
'vercel.deployments_list',
{
name: 'vercel.deployments_list',
description: 'List Vercel deployments',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' },
limit: { type: 'number' },
cursor: { type: 'string' }
},
required: ['project_id']
}
},
this.deploymentsList.bind(this)
);
registry.registerTool(
'vercel.deployment_get',
{
name: 'vercel.deployment_get',
description: 'Get deployment details',
inputSchema: {
type: 'object',
properties: {
deployment_id: { type: 'string' }
},
required: ['deployment_id']
}
},
this.deploymentGet.bind(this)
);
registry.registerTool(
'vercel.deployment_cancel',
{
name: 'vercel.deployment_cancel',
description: 'Cancel a deployment',
inputSchema: {
type: 'object',
properties: {
deployment_id: { type: 'string' }
},
required: ['deployment_id']
}
},
this.deploymentCancel.bind(this)
);
registry.registerTool(
'vercel.promote',
{
name: 'vercel.promote',
description: 'Promote deployment to production',
inputSchema: {
type: 'object',
properties: {
deployment_id: { type: 'string' }
},
required: ['deployment_id']
}
},
this.promote.bind(this)
);
registry.registerTool(
'vercel.logs',
{
name: 'vercel.logs',
description: 'Get deployment logs',
inputSchema: {
type: 'object',
properties: {
deployment_id: { type: 'string' },
since: { type: 'string' },
tail: { type: 'number' }
},
required: ['deployment_id']
}
},
this.logs.bind(this)
);
registry.registerTool(
'vercel.env_list',
{
name: 'vercel.env_list',
description: 'List environment variables',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' }
},
required: ['project_id']
}
},
this.envList.bind(this)
);
registry.registerTool(
'vercel.env_set',
{
name: 'vercel.env_set',
description: 'Set environment variables',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' },
vars: { type: 'object' }
},
required: ['project_id', 'vars']
}
},
this.envSet.bind(this)
);
registry.registerTool(
'vercel.domains_list',
{
name: 'vercel.domains_list',
description: 'List project domains',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' }
},
required: ['project_id']
}
},
this.domainsList.bind(this)
);
registry.registerTool(
'vercel.domain_add',
{
name: 'vercel.domain_add',
description: 'Add domain to project',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' },
name: { type: 'string' }
},
required: ['project_id', 'name']
}
},
this.domainAdd.bind(this)
);
registry.registerTool(
'vercel.domain_remove',
{
name: 'vercel.domain_remove',
description: 'Remove domain from project',
inputSchema: {
type: 'object',
properties: {
project_id: { type: 'string' },
name: { type: 'string' }
},
required: ['project_id', 'name']
}
},
this.domainRemove.bind(this)
);
}
private async projectsList(params: {
team_id?: string;
}): Promise<ListProjectsResponse> {
const response = await this.makeRequest<any>(
'GET',
'/v9/projects',
undefined,
params.team_id ? { teamId: params.team_id } : undefined
);
const projects: VercelProject[] = response.projects.map((p: any) => ({
id: p.id,
name: p.name,
slug: p.name // Vercel uses name as slug
}));
return { projects };
}
private async projectGet(params: {
project_id: string;
}): Promise<GetProjectResponse> {
const response = await this.makeRequest<any>(
'GET',
`/v9/projects/${params.project_id}`
);
return {
id: response.id,
name: response.name,
slug: response.name,
framework: response.framework,
targets: response.targets || ['production', 'preview'],
env: response.env ? Object.keys(response.env) : []
};
}
private async deploy(params: {
project_id: string;
source: { git_sha?: string; build_id?: string };
target?: DeploymentTarget;
}): Promise<DeployResponse> {
const deploymentData: any = {
name: params.project_id,
target: params.target || 'preview',
gitSource: params.source.git_sha ? {
ref: params.source.git_sha,
type: 'github'
} : undefined
};
const response = await this.makeRequest<any>(
'POST',
'/v13/deployments',
deploymentData
);
return {
deployment_id: response.id,
url: response.url,
status: response.readyState
};
}
private async redeploy(params: {
project_id: string;
from_deployment_id?: string;
from_git_sha?: string;
target?: DeploymentTarget;
}): Promise<RedeployResponse> {
let source: any = {};
if (params.from_deployment_id) {
// Get deployment details to copy
const deployment = await this.deploymentGet({
deployment_id: params.from_deployment_id
});
if (deployment.git_sha) {
source.git_sha = deployment.git_sha;
}
} else if (params.from_git_sha) {
source.git_sha = params.from_git_sha;
}
return this.deploy({
project_id: params.project_id,
source,
target: params.target
});
}
private async deploymentsList(params: {
project_id: string;
limit?: number;
cursor?: string;
}): Promise<ListDeploymentsResponse> {
const response = await this.makeRequest<any>(
'GET',
'/v6/deployments',
undefined,
{
projectId: params.project_id,
limit: params.limit || 20,
until: params.cursor
}
);
const deployments: VercelDeployment[] = response.deployments.map((d: any) => ({
id: d.uid,
created_at: new Date(d.created).toISOString(),
ready_state: d.readyState,
url: d.url,
git_sha: d.meta?.githubCommitSha
}));
return {
deployments,
next_cursor: response.pagination?.next
};
}
private async deploymentGet(params: {
deployment_id: string;
}): Promise<GetDeploymentResponse> {
const response = await this.makeRequest<any>(
'GET',
`/v13/deployments/${params.deployment_id}`
);
return {
id: response.id || response.uid,
project_id: response.projectId,
created_at: new Date(response.created || Date.now()).toISOString(),
ready_state: response.readyState,
inspector_url: response.inspectorUrl,
url: response.url,
git_sha: response.meta?.githubCommitSha
};
}
private async deploymentCancel(params: {
deployment_id: string;
}): Promise<{ ok: true }> {
await this.makeRequest<any>(
'PATCH',
`/v12/deployments/${params.deployment_id}/cancel`
);
return { ok: true };
}
private async promote(params: {
deployment_id: string;
}): Promise<PromoteResponse> {
const deployment = await this.deploymentGet(params);
// Promote by creating an alias
const response = await this.makeRequest<any>(
'POST',
`/v2/deployments/${params.deployment_id}/aliases`,
{
alias: deployment.url?.replace('https://', '').replace('.vercel.app', '')
}
);
return {
ok: true,
url: `https://${response.alias}`
};
}
private async logs(params: {
deployment_id: string;
since?: string;
tail?: number;
}): Promise<LogsResponse> {
const response = await this.makeRequest<any[]>(
'GET',
`/v2/deployments/${params.deployment_id}/events`,
undefined,
{
since: params.since ? new Date(params.since).getTime() : undefined,
limit: params.tail || 100
}
);
const lines: VercelLogLine[] = response.map(event => ({
ts: new Date(event.created).toISOString(),
message: event.text || event.payload?.text || ''
}));
return { lines };
}
private async envList(params: {
project_id: string;
}): Promise<EnvListResponse> {
const response = await this.makeRequest<any>(
'GET',
`/v8/projects/${params.project_id}/env`
);
const vars: Record<string, string> = {};
for (const envVar of response.envs) {
vars[envVar.key] = envVar.value;
}
return { vars };
}
private async envSet(params: {
project_id: string;
vars: Record<string, string>;
}): Promise<EnvSetResponse> {
// Vercel requires individual API calls for each env var
for (const [key, value] of Object.entries(params.vars)) {
await this.makeRequest<any>(
'POST',
`/v10/projects/${params.project_id}/env`,
{
key,
value,
target: ['production', 'preview', 'development'],
type: 'encrypted'
}
);
}
return { ok: true };
}
private async domainsList(params: {
project_id: string;
}): Promise<ListDomainsResponse> {
const response = await this.makeRequest<any>(
'GET',
`/v9/projects/${params.project_id}/domains`
);
const domains: VercelDomain[] = response.domains.map((d: any) => ({
name: d.name,
verified: d.verified,
apex: d.apexName === d.name
}));
return { domains };
}
private async domainAdd(params: {
project_id: string;
name: string;
}): Promise<AddDomainResponse> {
const response = await this.makeRequest<any>(
'POST',
`/v10/projects/${params.project_id}/domains`,
{
name: params.name
}
);
return {
name: response.name,
verified: response.verified
};
}
private async domainRemove(params: {
project_id: string;
name: string;
}): Promise<RemoveDomainResponse> {
await this.makeRequest<any>(
'DELETE',
`/v9/projects/${params.project_id}/domains/${params.name}`
);
return { ok: true };
}
}