Vercel MCP Server

import { z } from "zod"; import { handleResponse } from "../utils/response.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; export function registerDeploymentTools(server: McpServer) { // Create new deployment server.tool( "create_deployment", "Create a new deployment with all required data", { name: z.string().describe("Project name used in the deployment URL"), project: z.string().optional().describe("Target project identifier (overrides name)"), files: z.array(z.object({ data: z.string(), encoding: z.string().optional(), file: z.string() })).optional().describe("Files to be deployed"), gitMetadata: z.object({ remoteUrl: z.string().optional(), commitAuthorName: z.string().optional(), commitMessage: z.string().optional(), commitRef: z.string().optional(), commitSha: z.string().optional(), dirty: z.boolean().optional() }).optional().describe("Git metadata for the deployment"), gitSource: z.object({ type: z.string(), repoId: z.union([z.string(), z.number()]), ref: z.string().optional(), sha: z.string().optional() }).optional().describe("Git repository source"), target: z.string().optional().describe("Deployment target (production, preview, staging)"), deploymentId: z.string().optional().describe("Existing deployment ID to redeploy"), meta: z.record(z.any()).optional().describe("Deployment metadata"), projectSettings: z.object({ buildCommand: z.string().nullable().optional(), devCommand: z.string().nullable().optional(), framework: z.string().nullable().optional(), installCommand: z.string().nullable().optional(), nodeVersion: z.string().optional(), outputDirectory: z.string().nullable().optional(), rootDirectory: z.string().nullable().optional(), serverlessFunctionRegion: z.string().nullable().optional(), skipGitConnectDuringLink: z.boolean().optional(), sourceFilesOutsideRootDirectory: z.boolean().optional() }).optional().describe("Project settings for the deployment"), forceNew: z.boolean().optional().describe("Force new deployment even if similar exists"), skipAutoDetectionConfirmation: z.boolean().optional().describe("Skip framework detection confirmation"), customEnvironmentSlugOrId: z.string().optional().describe("Custom environment to deploy to"), monorepoManager: z.string().nullable().optional().describe("Monorepo manager being used"), withLatestCommit: z.boolean().optional().describe("Force latest commit when redeploying"), teamId: z.string().optional().describe("Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("Team slug to perform the request on behalf of") }, async ({ name, project, files, gitMetadata, gitSource, target, deploymentId, meta, projectSettings, forceNew, skipAutoDetectionConfirmation, customEnvironmentSlugOrId, monorepoManager, withLatestCommit, teamId, slug }) => { const url = new URL(`${BASE_URL}/v13/deployments`); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); if (forceNew) url.searchParams.append("forceNew", "1"); if (skipAutoDetectionConfirmation) url.searchParams.append("skipAutoDetectionConfirmation", "1"); const response = await fetch(url.toString(), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, body: JSON.stringify({ name, project, files, gitMetadata, gitSource, target, deploymentId, meta, projectSettings, customEnvironmentSlugOrId, monorepoManager, withLatestCommit }), }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployment created:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Cancel deployment server.tool( "cancel_deployment", "Cancel a deployment which is currently building", { id: z.string().describe("The unique identifier of the deployment"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ id, teamId, slug }) => { const url = new URL(`${BASE_URL}/v12/deployments/${id}/cancel`); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { method: "PATCH", headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployment canceled:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Get deployment by ID or URL server.tool( "get_deployment", "Get deployment by ID or URL", { idOrUrl: z.string().describe("The unique identifier or hostname of the deployment"), withGitRepoInfo: z.string().optional().describe("Whether to add gitRepo information"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ idOrUrl, withGitRepoInfo, teamId, slug }) => { const url = new URL(`${BASE_URL}/v13/deployments/${idOrUrl}`); if (withGitRepoInfo) url.searchParams.append("withGitRepoInfo", withGitRepoInfo); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployment details:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Delete deployment server.tool( "delete_deployment", "Delete a deployment by ID or URL", { id: z.string().describe("The unique identifier of the deployment"), url: z.string().optional().describe("A Deployment or Alias URL (overrides ID if provided)"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ id, url, teamId, slug }) => { const baseUrl = new URL(`${BASE_URL}/v13/deployments/${id}`); if (url) baseUrl.searchParams.append("url", url); if (teamId) baseUrl.searchParams.append("teamId", teamId); if (slug) baseUrl.searchParams.append("slug", slug); const response = await fetch(baseUrl.toString(), { method: "DELETE", headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployment deleted:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Get deployment events server.tool( "get_deployment_events", "Get build logs and events for a deployment", { idOrUrl: z.string().describe("The unique identifier or hostname of the deployment"), direction: z.enum(["forward", "backward"]).optional().describe("Order of the returned events based on timestamp"), follow: z.number().optional().describe("Return live events as they happen (1 to enable)"), limit: z.number().optional().describe("Maximum number of events to return (-1 for all)"), name: z.string().optional().describe("Deployment build ID"), since: z.number().optional().describe("Timestamp to start pulling logs from"), until: z.number().optional().describe("Timestamp to pull logs until"), statusCode: z.string().optional().describe("HTTP status code range to filter events by (e.g. '5xx')"), delimiter: z.number().optional().describe("Delimiter option"), builds: z.number().optional().describe("Builds option"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ idOrUrl, direction, follow, limit, name, since, until, statusCode, delimiter, builds, teamId, slug }) => { const url = new URL(`${BASE_URL}/v3/deployments/${idOrUrl}/events`); // Add all optional query parameters if (direction) url.searchParams.append("direction", direction); if (follow !== undefined) url.searchParams.append("follow", follow.toString()); if (limit !== undefined) url.searchParams.append("limit", limit.toString()); if (name) url.searchParams.append("name", name); if (since !== undefined) url.searchParams.append("since", since.toString()); if (until !== undefined) url.searchParams.append("until", until.toString()); if (statusCode) url.searchParams.append("statusCode", statusCode); if (delimiter !== undefined) url.searchParams.append("delimiter", delimiter.toString()); if (builds !== undefined) url.searchParams.append("builds", builds.toString()); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployment events:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Update deployment integration action server.tool( "update_deployment_integration", "Update deployment integration action status", { deploymentId: z.string().describe("The deployment ID"), integrationConfigurationId: z.string().describe("The integration configuration ID"), resourceId: z.string().describe("The resource ID"), action: z.string().describe("The action to update"), status: z.enum(["running", "succeeded", "failed"]).describe("The status of the action"), statusText: z.string().optional().describe("Additional status text"), outcomes: z.array(z.object({ kind: z.string(), secrets: z.array(z.object({ name: z.string(), value: z.string() })).optional() })).optional().describe("Action outcomes"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ deploymentId, integrationConfigurationId, resourceId, action, status, statusText, outcomes, teamId, slug }) => { const url = new URL( `${BASE_URL}/v1/deployments/${deploymentId}/integrations/${integrationConfigurationId}/resources/${resourceId}/actions/${action}` ); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, body: JSON.stringify({ status, statusText, outcomes }), }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Integration action updated:\n${JSON.stringify(data, null, 2)}` }], }; } ); // List deployment files server.tool( "list_deployment_files", "Get file structure of a deployment's source code", { id: z.string().describe("The unique deployment identifier"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ id, teamId, slug }) => { const url = new URL(`${BASE_URL}/v6/deployments/${id}/files`); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployment files:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Upload deployment files server.tool( "upload_deployment_files", "Upload files required for deployment", { content: z.string().describe("The file content to upload"), size: z.number().describe("The file size in bytes"), digest: z.string().max(40).describe("The file SHA1 for integrity check"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ content, size, digest, teamId, slug }) => { const url = new URL(`${BASE_URL}/v2/files`); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { method: "POST", headers: { "Content-Length": size.toString(), "x-vercel-digest": digest, Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, body: content, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `File upload result:\n${JSON.stringify(data, null, 2)}` }], }; } ); // Get deployment file contents server.tool( "get_deployment_file", "Get contents of a specific deployment file", { id: z.string().describe("The unique deployment identifier"), fileId: z.string().describe("The unique file identifier"), path: z.string().optional().describe("Path to the file (only for Git deployments)"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ id, fileId, path, teamId, slug }) => { const url = new URL(`${BASE_URL}/v7/deployments/${id}/files/${fileId}`); if (path) url.searchParams.append("path", path); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `File contents:\n${JSON.stringify(data, null, 2)}` }], }; } ); // List deployments server.tool( "list_deployment", "List deployments under the authenticated user or team", { app: z.string().optional().describe("Name of the deployment"), from: z.number().optional().describe("Get deployments created after this timestamp (deprecated)"), limit: z.number().optional().describe("Maximum number of deployments to list"), projectId: z.string().optional().describe("Filter deployments from the given ID or name"), target: z.string().optional().describe("Filter deployments based on the environment"), to: z.number().optional().describe("Get deployments created before this timestamp (deprecated)"), users: z.string().optional().describe("Filter deployments based on users who created them"), since: z.number().optional().describe("Get deployments created after this timestamp"), until: z.number().optional().describe("Get deployments created before this timestamp"), state: z.string().optional().describe("Filter by state (BUILDING, ERROR, INITIALIZING, QUEUED, READY, CANCELED)"), rollbackCandidate: z.boolean().optional().describe("Filter deployments based on rollback candidacy"), teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), slug: z.string().optional().describe("The Team slug to perform the request on behalf of") }, async ({ app, from, limit, projectId, target, to, users, since, until, state, rollbackCandidate, teamId, slug }) => { const url = new URL(`${BASE_URL}/v6/deployments`); // Add all optional query parameters if (app) url.searchParams.append("app", app); if (from !== undefined) url.searchParams.append("from", from.toString()); if (limit !== undefined) url.searchParams.append("limit", limit.toString()); if (projectId) url.searchParams.append("projectId", projectId); if (target) url.searchParams.append("target", target); if (to !== undefined) url.searchParams.append("to", to.toString()); if (users) url.searchParams.append("users", users); if (since !== undefined) url.searchParams.append("since", since.toString()); if (until !== undefined) url.searchParams.append("until", until.toString()); if (state) url.searchParams.append("state", state); if (rollbackCandidate !== undefined) url.searchParams.append("rollbackCandidate", rollbackCandidate.toString()); if (teamId) url.searchParams.append("teamId", teamId); if (slug) url.searchParams.append("slug", slug); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, }, }); const data = await handleResponse(response); return { content: [{ type: "text", text: `Deployments list:\n${JSON.stringify(data, null, 2)}` }], }; } ); }