├── src ├── utils │ └── response.ts ├── tool-groups │ ├── integrations │ │ └── index.ts │ ├── projects │ │ └── index.ts │ ├── domains │ │ └── index.ts │ ├── access │ │ └── index.ts │ └── infrastructure │ │ └── index.ts ├── components │ ├── speed-insights.ts │ ├── users.ts │ ├── certs.ts │ ├── projectmembers.ts │ ├── auth.ts │ ├── webhooks.ts │ ├── secrets.ts │ ├── dns.ts │ ├── aliases.ts │ ├── integrations.ts │ ├── environments.ts │ ├── artifacts.ts │ ├── logDrains.ts │ ├── teams.ts │ ├── marketplace.ts │ ├── security.ts │ ├── domains.ts │ ├── edge-config.ts │ └── accessgroups.ts ├── index.ts ├── tool-manager.ts └── resources.ts ├── tsconfig.json ├── package.json ├── .gitignore └── README.md /src/utils/response.ts: -------------------------------------------------------------------------------- 1 | export async function handleResponse(response: Response): Promise { 2 | if (!response.ok) { 3 | const errorText = await response.text(); 4 | throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`); 5 | } 6 | return response.json(); 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "rootDir": "src", 10 | "declaration": false, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vercel-mcp", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node dist/index.js", 8 | "dev": "tsc -w" 9 | }, 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "latest", 12 | "zod": "latest" 13 | }, 14 | "devDependencies": { 15 | "typescript": "latest", 16 | "@types/node": "latest" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | package-lock.json 7 | yarn.lock 8 | 9 | # Build output 10 | dist/ 11 | build/ 12 | *.tsbuildinfo 13 | 14 | # Environment variables and secrets 15 | .env 16 | .env.local 17 | .env.*.local 18 | src/config/constants.ts 19 | src/index.ts 20 | 21 | # IDE and editor files 22 | .idea/ 23 | .vscode/ 24 | *.swp 25 | *.swo 26 | .DS_Store 27 | Thumbs.db 28 | 29 | # Logs 30 | logs/ 31 | *.log 32 | 33 | # Testing 34 | coverage/ 35 | .nyc_output/ 36 | 37 | # Temporary files 38 | tmp/ 39 | temp/ 40 | 41 | # Debug 42 | .debug/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity -------------------------------------------------------------------------------- /src/tool-groups/integrations/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { registerIntegrationTools } from "../../components/integrations.js"; 3 | import { registerMarketplaceTools } from "../../components/marketplace.js"; 4 | import { registerArtifactTools } from "../../components/artifacts.js"; 5 | 6 | export async function register(server: McpServer): Promise { 7 | registerIntegrationTools(server); 8 | registerMarketplaceTools(server); 9 | registerArtifactTools(server); 10 | 11 | return [ 12 | // Integrations 13 | "int_delete", 14 | "int_list", 15 | "int_gitns", 16 | "int_searchRepo", 17 | "int_get", 18 | "int_updateAction", 19 | "mcp_integration", 20 | 21 | // Marketplace 22 | "create_marketplace_event", 23 | "get_marketplace_account", 24 | "get_marketplace_invoice", 25 | "get_marketplace_member", 26 | "import_marketplace_resource", 27 | "submit_marketplace_billing", 28 | "submit_marketplace_invoice", 29 | "update_marketplace_secrets", 30 | "marketplace_sso_token_exchange", 31 | "submit_marketplace_balance", 32 | "marketplace_invoice_action", 33 | "mcp_marketplace", 34 | 35 | // Artifacts (Build Outputs, Caching, Binaries) 36 | "check_artifact", 37 | "download_artifact", 38 | "get_artifact_status", 39 | "query_artifacts", 40 | "record_artifact_events", 41 | "upload_artifact", 42 | "mcp_artifact" 43 | ]; 44 | } -------------------------------------------------------------------------------- /src/tool-groups/projects/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { registerProjectTools } from "../../components/projects.js"; 3 | import { registerDeploymentTools } from "../../components/deployments.js"; 4 | 5 | export async function register(server: McpServer): Promise { 6 | registerProjectTools(server); 7 | registerDeploymentTools(server); 8 | 9 | return [ 10 | // Project Management 11 | "list_projects", 12 | "create_project", 13 | "delete_project", 14 | "get_project_domain", 15 | "update_project", 16 | "mcp_project", 17 | 18 | // Project Members 19 | "add_project_member", 20 | "list_project_members", 21 | "remove_project_member", 22 | "mcp_project_member", 23 | 24 | // Project Transfers 25 | "request_project_transfer", 26 | "accept_project_transfer", 27 | "mcp_project_transfer", 28 | 29 | // Deployments 30 | "list_deployments", 31 | "promote_deployment", 32 | "get_promotion_aliases", 33 | "pause_project", 34 | "mcp_deployment", 35 | 36 | // Deployment Management 37 | "create_deployment", 38 | "cancel_deployment", 39 | "get_deployment", 40 | "delete_deployment", 41 | "get_deployment_events", 42 | "update_deployment_integration", 43 | "mcp_deployment_management", 44 | 45 | // Deployment Files 46 | "list_deployment_files", 47 | "upload_deployment_files", 48 | "get_deployment_file", 49 | "list_deployment", 50 | "mcp_deployment_files" 51 | ]; 52 | } -------------------------------------------------------------------------------- /src/tool-groups/domains/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { registerDomainTools } from "../../components/domains.js"; 3 | import { registerDnsTools } from "../../components/dns.js"; 4 | import { registerCertTools } from "../../components/certs.js"; 5 | import { registerAliasTools } from "../../components/aliases.js"; 6 | 7 | export async function register(server: McpServer): Promise { 8 | registerDomainTools(server); 9 | registerDnsTools(server); 10 | registerCertTools(server); 11 | registerAliasTools(server); 12 | 13 | return [ 14 | // Domain Operations 15 | "add_domain", 16 | "remove_domain", 17 | "get_domain", 18 | "list_domains", 19 | "mcp_domains", 20 | 21 | // Domain Registry 22 | "domain_check", 23 | "domain_price", 24 | "domain_config", 25 | "domain_registry", 26 | "domain_get", 27 | "domain_list", 28 | "domain_buy", 29 | "domain_register", 30 | "domain_remove", 31 | "domain_update", 32 | "mcp_registry", 33 | 34 | // DNS Management 35 | "create_dns_record", 36 | "delete_dns_record", 37 | "list_dns_records", 38 | "update_dns_record", 39 | "mcp_dns", 40 | 41 | // Certificates (SSL/TLS) 42 | "get_cert", 43 | "issue_cert", 44 | "remove_cert", 45 | "upload_cert", 46 | "mcp_certificate", 47 | 48 | // Aliases 49 | "assign_alias", 50 | "delete_alias", 51 | "get_alias", 52 | "list_aliases", 53 | "list_deployment_aliases", 54 | "mcp_alias" 55 | ]; 56 | } -------------------------------------------------------------------------------- /src/components/speed-insights.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | // Common schemas 7 | const VitalsSchema = z.object({ 8 | dsn: z.string().describe("Project's unique identifier (VERCEL_ANALYTICS_ID)"), 9 | event_name: z.string().describe("Name of the vital to track"), 10 | href: z.string().describe("Full URL for the deployed application"), 11 | id: z.string().describe("Unique identifier for the vital"), 12 | page: z.string().describe("Framework's file name path"), 13 | speed: z.string().describe("Connection information from visitor device"), 14 | value: z.string().describe("Value of the vital to track") 15 | }); 16 | 17 | export function registerSpeedInsightsTools(server: McpServer) { 18 | // Send Web Vitals 19 | server.tool( 20 | "send_web_vitals", 21 | "Send web vitals data to Speed Insights API (Deprecated: Use @vercel/speed-insights package instead)", 22 | { 23 | vitals: VitalsSchema.describe("Web vitals data") 24 | }, 25 | async ({ vitals }) => { 26 | const response = await fetch(`${BASE_URL}/v1/vitals`, { 27 | method: "POST", 28 | headers: { 29 | "Content-Type": "application/json", 30 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 31 | }, 32 | body: JSON.stringify(vitals), 33 | }); 34 | 35 | await handleResponse(response); 36 | return { 37 | content: [ 38 | { type: "text", text: "Web vitals data sent successfully" }, 39 | { type: "text", text: "⚠️ Warning: This API is deprecated. Please use @vercel/speed-insights package instead." } 40 | ], 41 | }; 42 | } 43 | ); 44 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { ToolManager } from "./tool-manager.js"; 4 | import { registerResources } from "./resources.js"; 5 | 6 | export const BASE_URL = "https://api.vercel.com"; 7 | export const DEFAULT_ACCESS_TOKEN = "Your_Access_Token"; // Replace with your actual token 8 | 9 | // Utility function to handle responses 10 | export async function handleResponse(response: Response): Promise { 11 | if (!response.ok) { 12 | const errorText = await response.text(); 13 | throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`); 14 | } 15 | return response.json(); 16 | } 17 | 18 | async function main() { 19 | try { 20 | // Create an MCP server instance for Vercel tools 21 | const server = new McpServer({ 22 | name: "vercel-tools", 23 | version: "1.0.0" 24 | }); 25 | 26 | // Register resources (these are always available) 27 | registerResources(server); 28 | 29 | // Create tool manager 30 | const toolManager = new ToolManager(server); 31 | 32 | // Load only essential groups initially 33 | await toolManager.loadGroup('projects'); // Most commonly used 34 | await toolManager.loadGroup('infrastructure'); // Contains core functionality 35 | 36 | // Create transport 37 | const transport = new StdioServerTransport(); 38 | 39 | // Connect transport to server 40 | await server.connect(transport); 41 | 42 | // Log startup 43 | console.error("Vercel MCP Server running on stdio"); 44 | 45 | // Keep the process running 46 | process.stdin.resume(); 47 | 48 | // Handle process termination 49 | process.on('SIGINT', () => { 50 | console.error("Shutting down..."); 51 | process.exit(0); 52 | }); 53 | 54 | } catch (error) { 55 | console.error("Fatal error:", error); 56 | process.exit(1); 57 | } 58 | } 59 | 60 | main(); 61 | -------------------------------------------------------------------------------- /src/tool-groups/access/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { registerTeamTools } from "../../components/teams.js"; 3 | import { registerUserTools } from "../../components/users.js"; 4 | import { registerAuthTools } from "../../components/auth.js"; 5 | import { registerAccessGroupTools } from "../../components/accessgroups.js"; 6 | import { registerSecurityTools } from "../../components/security.js"; 7 | 8 | export async function register(server: McpServer): Promise { 9 | registerTeamTools(server); 10 | registerUserTools(server); 11 | registerAuthTools(server); 12 | registerAccessGroupTools(server); 13 | registerSecurityTools(server); 14 | 15 | return [ 16 | // Access Groups 17 | "create_access_group_project", 18 | "create_access_group", 19 | "delete_access_group_project", 20 | "delete_access_group", 21 | "list_access_groups", 22 | "list_access_group_members", 23 | "list_access_group_projects", 24 | "get_access_group", 25 | "get_access_group_project", 26 | "update_access_group", 27 | "update_access_group_project", 28 | "mcp_access_group", 29 | 30 | // Authentication & Authorization 31 | "create_auth_token", 32 | "delete_auth_token", 33 | "get_auth_token", 34 | "list_auth_tokens", 35 | "sso_token_exchange", 36 | "mcp_auth", 37 | 38 | // User Management 39 | "delete_user", 40 | "get_user", 41 | "list_user_events", 42 | "mcp_user", 43 | 44 | // Team Management 45 | "create_team", 46 | "delete_team", 47 | "get_team", 48 | "list_teams", 49 | "list_team_members", 50 | "invite_team_member", 51 | "remove_team_member", 52 | "update_team_member", 53 | "update_team", 54 | "mcp_team", 55 | 56 | // Firewall & Security 57 | "create_firewall_bypass", 58 | "delete_firewall_bypass", 59 | "get_firewall_bypass", 60 | "get_attack_status", 61 | "update_attack_mode", 62 | "get_firewall_config", 63 | "update_firewall_config", 64 | "put_firewall_config", 65 | "mcp_security" 66 | ]; 67 | } -------------------------------------------------------------------------------- /src/tool-groups/infrastructure/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { registerEdgeConfigTools } from "../../components/edge-config.js"; 3 | import { registerSecretTools } from "../../components/secrets.js"; 4 | import { registerEnvironmentTools } from "../../components/environments.js"; 5 | import { registerWebhookTools } from "../../components/webhooks.js"; 6 | import { registerLogDrainTools } from "../../components/logDrains.js"; 7 | import { registerSpeedInsightsTools } from "../../components/speed-insights.js"; 8 | 9 | export async function register(server: McpServer): Promise { 10 | registerEdgeConfigTools(server); 11 | registerSecretTools(server); 12 | registerEnvironmentTools(server); 13 | registerWebhookTools(server); 14 | registerLogDrainTools(server); 15 | registerSpeedInsightsTools(server); 16 | 17 | return [ 18 | // Edge Configurations 19 | "create_edge_config", 20 | "create_edge_config_token", 21 | "list_edge_configs", 22 | "get_edge_config", 23 | "update_edge_config", 24 | "delete_edge_config", 25 | "list_edge_config_items", 26 | "get_edge_config_item", 27 | "update_edge_config_items", 28 | "get_edge_config_schema", 29 | "update_edge_config_schema", 30 | "delete_edge_config_schema", 31 | "list_edge_config_tokens", 32 | "get_edge_config_token", 33 | "delete_edge_config_tokens", 34 | "list_edge_config_backups", 35 | "get_edge_config_backup", 36 | "mcp_edge_config", 37 | 38 | // Secrets Management 39 | "create_secret", 40 | "update_secret_name", 41 | "delete_secret", 42 | "get_secret", 43 | "list_secrets", 44 | "mcp_secret", 45 | 46 | // Environment Variables 47 | "add_env", 48 | "update_env", 49 | "delete_env", 50 | "get_env", 51 | "list_env", 52 | "mcp_env", 53 | 54 | // Environments (Projects/Deployments) 55 | "create_environment", 56 | "delete_environment", 57 | "get_environment", 58 | "list_environments", 59 | "update_environment", 60 | "mcp_environment", 61 | 62 | // Webhooks 63 | "create_webhook", 64 | "delete_webhook", 65 | "list_webhooks", 66 | "get_webhook", 67 | "mcp_webhook", 68 | 69 | // Logging 70 | "logdrain_create", 71 | "logdrain_createIntegration", 72 | "logdrain_delete", 73 | "logdrain_deleteIntegration", 74 | "logdrain_get", 75 | "logdrain_list", 76 | "logdrain_listIntegration", 77 | "mcp_logdrain", 78 | 79 | // Performance Monitoring 80 | "send_web_vitals", 81 | "mcp_speed_insights" 82 | ]; 83 | } -------------------------------------------------------------------------------- /src/components/users.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerUserTools(server: McpServer) { 7 | // Delete User Account 8 | server.tool( 9 | "delete_user", 10 | "Initiates the deletion process for the currently authenticated User", 11 | { 12 | reasons: z.array( 13 | z.object({ 14 | slug: z.string().describe("Reason identifier"), 15 | description: z.string().describe("Detailed description of the reason") 16 | }) 17 | ).optional().describe("Optional array of objects that describe the reason why the User account is being deleted") 18 | }, 19 | async ({ reasons }) => { 20 | const response = await fetch(`${BASE_URL}/v1/user`, { 21 | method: "DELETE", 22 | headers: { 23 | "Content-Type": "application/json", 24 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 25 | }, 26 | ...(reasons && { body: JSON.stringify({ reasons }) }) 27 | }); 28 | 29 | const data = await handleResponse(response); 30 | return { 31 | content: [ 32 | { type: "text", text: `User deletion initiated:\n${JSON.stringify(data, null, 2)}` }, 33 | ], 34 | }; 35 | } 36 | ); 37 | 38 | // Get User Information 39 | server.tool( 40 | "get_user", 41 | "Retrieves information related to the currently authenticated User", 42 | {}, 43 | async () => { 44 | const response = await fetch(`${BASE_URL}/v2/user`, { 45 | headers: { 46 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 47 | }, 48 | }); 49 | 50 | const data = await handleResponse(response); 51 | return { 52 | content: [ 53 | { type: "text", text: `User information:\n${JSON.stringify(data, null, 2)}` }, 54 | ], 55 | }; 56 | } 57 | ); 58 | 59 | // List User Events 60 | server.tool( 61 | "list_user_events", 62 | "Retrieves a list of events generated by the User on Vercel", 63 | { 64 | limit: z.number().optional().describe("Maximum number of items which may be returned"), 65 | since: z.string().optional().describe("Timestamp to only include items created since then (e.g. 2019-12-08T10:00:38.976Z)"), 66 | until: z.string().optional().describe("Timestamp to only include items created until then (e.g. 2019-12-09T23:00:38.976Z)"), 67 | types: z.string().optional().describe("Comma-delimited list of event types to filter the results by (e.g. login,team-member-join,domain-buy)"), 68 | userId: z.string().optional().describe("When retrieving events for a Team, filter events by a specific member"), 69 | withPayload: z.string().optional().describe("When set to true, includes the payload field for each event"), 70 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 71 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 72 | }, 73 | async ({ limit, since, until, types, userId, withPayload, teamId, slug }) => { 74 | const url = new URL(`${BASE_URL}/v3/events`); 75 | 76 | if (limit) url.searchParams.append("limit", limit.toString()); 77 | if (since) url.searchParams.append("since", since); 78 | if (until) url.searchParams.append("until", until); 79 | if (types) url.searchParams.append("types", types); 80 | if (userId) url.searchParams.append("userId", userId); 81 | if (withPayload) url.searchParams.append("withPayload", withPayload); 82 | if (teamId) url.searchParams.append("teamId", teamId); 83 | if (slug) url.searchParams.append("slug", slug); 84 | 85 | const response = await fetch(url.toString(), { 86 | headers: { 87 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 88 | }, 89 | }); 90 | 91 | const data = await handleResponse(response); 92 | return { 93 | content: [ 94 | { type: "text", text: `User events:\n${JSON.stringify(data, null, 2)}` }, 95 | ], 96 | }; 97 | } 98 | ); 99 | } -------------------------------------------------------------------------------- /src/components/certs.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerCertTools(server: McpServer) { 7 | // Get cert by id 8 | server.tool( 9 | "get_cert", 10 | "Get certificate by ID", 11 | { 12 | id: z.string().describe("The cert id"), 13 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 14 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 15 | }, 16 | async ({ id, teamId, slug }) => { 17 | const url = new URL(`${BASE_URL}/v7/certs/${id}`); 18 | if (teamId) url.searchParams.append("teamId", teamId); 19 | if (slug) url.searchParams.append("slug", slug); 20 | 21 | const response = await fetch(url.toString(), { 22 | headers: { 23 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 24 | }, 25 | }); 26 | 27 | const data = await handleResponse(response); 28 | return { 29 | content: [{ type: "text", text: `Certificate details:\n${JSON.stringify(data, null, 2)}` }], 30 | }; 31 | } 32 | ); 33 | 34 | // Issue a new cert 35 | server.tool( 36 | "issue_cert", 37 | "Issue a new certificate", 38 | { 39 | cns: z.array(z.string()).describe("The common names the cert should be issued for"), 40 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 41 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 42 | }, 43 | async ({ cns, teamId, slug }) => { 44 | const url = new URL(`${BASE_URL}/v7/certs`); 45 | if (teamId) url.searchParams.append("teamId", teamId); 46 | if (slug) url.searchParams.append("slug", slug); 47 | 48 | const response = await fetch(url.toString(), { 49 | method: "POST", 50 | headers: { 51 | "Content-Type": "application/json", 52 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 53 | }, 54 | body: JSON.stringify({ cns }), 55 | }); 56 | 57 | const data = await handleResponse(response); 58 | return { 59 | content: [{ type: "text", text: `Certificate issued:\n${JSON.stringify(data, null, 2)}` }], 60 | }; 61 | } 62 | ); 63 | 64 | // Remove cert 65 | server.tool( 66 | "remove_cert", 67 | "Remove a certificate", 68 | { 69 | id: z.string().describe("The cert id to remove"), 70 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 71 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 72 | }, 73 | async ({ id, teamId, slug }) => { 74 | const url = new URL(`${BASE_URL}/v7/certs/${id}`); 75 | if (teamId) url.searchParams.append("teamId", teamId); 76 | if (slug) url.searchParams.append("slug", slug); 77 | 78 | const response = await fetch(url.toString(), { 79 | method: "DELETE", 80 | headers: { 81 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 82 | }, 83 | }); 84 | 85 | const data = await handleResponse(response); 86 | return { 87 | content: [{ type: "text", text: `Certificate removed:\n${JSON.stringify(data, null, 2)}` }], 88 | }; 89 | } 90 | ); 91 | 92 | // Upload a cert 93 | server.tool( 94 | "upload_cert", 95 | "Upload a certificate", 96 | { 97 | ca: z.string().describe("The certificate authority"), 98 | key: z.string().describe("The certificate key"), 99 | cert: z.string().describe("The certificate"), 100 | skipValidation: z.boolean().optional().describe("Skip validation of the certificate"), 101 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 102 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 103 | }, 104 | async ({ ca, key, cert, skipValidation, teamId, slug }) => { 105 | const url = new URL(`${BASE_URL}/v7/certs`); 106 | if (teamId) url.searchParams.append("teamId", teamId); 107 | if (slug) url.searchParams.append("slug", slug); 108 | 109 | const response = await fetch(url.toString(), { 110 | method: "PUT", 111 | headers: { 112 | "Content-Type": "application/json", 113 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 114 | }, 115 | body: JSON.stringify({ ca, key, cert, skipValidation }), 116 | }); 117 | 118 | const data = await handleResponse(response); 119 | return { 120 | content: [{ type: "text", text: `Certificate uploaded:\n${JSON.stringify(data, null, 2)}` }], 121 | }; 122 | } 123 | ); 124 | } -------------------------------------------------------------------------------- /src/components/projectmembers.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | import { handleResponse, BASE_URL, DEFAULT_ACCESS_TOKEN } from "../index.js"; 4 | 5 | // Common parameter schemas 6 | const teamParams = { 7 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 8 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 9 | }; 10 | 11 | export function registerProjectMemberTools(server: McpServer) { 12 | // Add Project Member 13 | server.tool( 14 | "add_project_member", 15 | "Adds a new member to a project", 16 | { 17 | idOrName: z.string().describe("The ID or name of the Project"), 18 | uid: z.string().max(256).optional().describe("The ID of the team member that should be added to this project"), 19 | username: z.string().max(256).optional().describe("The username of the team member that should be added to this project"), 20 | email: z.string().email().optional().describe("The email of the team member that should be added to this project"), 21 | role: z.enum(["ADMIN", "PROJECT_DEVELOPER", "PROJECT_VIEWER"]).describe("The project role of the member that will be added"), 22 | ...teamParams 23 | }, 24 | async ({ idOrName, uid, username, email, role, teamId, slug }) => { 25 | const url = new URL(`${BASE_URL}/v1/projects/${idOrName}/members`); 26 | if (teamId) url.searchParams.append("teamId", teamId); 27 | if (slug) url.searchParams.append("slug", slug); 28 | 29 | const response = await fetch(url.toString(), { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 34 | }, 35 | body: JSON.stringify({ 36 | ...(uid && { uid }), 37 | ...(username && { username }), 38 | ...(email && { email }), 39 | role, 40 | }), 41 | }); 42 | 43 | const data = await handleResponse(response); 44 | return { 45 | content: [{ type: "text", text: `Project member added:\n${JSON.stringify(data, null, 2)}` }], 46 | }; 47 | } 48 | ); 49 | 50 | // List Project Members 51 | server.tool( 52 | "list_project_members", 53 | "Lists all members of a project", 54 | { 55 | idOrName: z.string().describe("The ID or name of the Project"), 56 | limit: z.number().min(1).max(100).optional().describe("Limit how many project members should be returned"), 57 | since: z.number().optional().describe("Timestamp in milliseconds to only include members added since then"), 58 | until: z.number().optional().describe("Timestamp in milliseconds to only include members added until then"), 59 | search: z.string().optional().describe("Search project members by their name, username, and email"), 60 | ...teamParams 61 | }, 62 | async ({ idOrName, limit, since, until, search, teamId, slug }) => { 63 | const url = new URL(`${BASE_URL}/v1/projects/${idOrName}/members`); 64 | const queryParams = new URLSearchParams(); 65 | 66 | if (limit) queryParams.append("limit", limit.toString()); 67 | if (since) queryParams.append("since", since.toString()); 68 | if (until) queryParams.append("until", until.toString()); 69 | if (search) queryParams.append("search", search); 70 | if (teamId) queryParams.append("teamId", teamId); 71 | if (slug) queryParams.append("slug", slug); 72 | 73 | const response = await fetch(`${url.toString()}?${queryParams.toString()}`, { 74 | headers: { 75 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 76 | }, 77 | }); 78 | 79 | const data = await handleResponse(response); 80 | return { 81 | content: [{ type: "text", text: `Project members:\n${JSON.stringify(data, null, 2)}` }], 82 | }; 83 | } 84 | ); 85 | 86 | // Remove Project Member 87 | server.tool( 88 | "remove_project_member", 89 | "Remove a member from a specific project", 90 | { 91 | idOrName: z.string().describe("The ID or name of the Project"), 92 | uid: z.string().describe("The user ID of the member"), 93 | ...teamParams 94 | }, 95 | async ({ idOrName, uid, teamId, slug }) => { 96 | const url = new URL(`${BASE_URL}/v1/projects/${idOrName}/members/${uid}`); 97 | if (teamId) url.searchParams.append("teamId", teamId); 98 | if (slug) url.searchParams.append("slug", slug); 99 | 100 | const response = await fetch(url.toString(), { 101 | method: "DELETE", 102 | headers: { 103 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 104 | }, 105 | }); 106 | 107 | const data = await handleResponse(response); 108 | return { 109 | content: [{ type: "text", text: `Project member removed:\n${JSON.stringify(data, null, 2)}` }], 110 | }; 111 | } 112 | ); 113 | } -------------------------------------------------------------------------------- /src/components/auth.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerAuthTools(server: McpServer) { 7 | // Create Auth Token 8 | server.tool( 9 | "create_auth_token", 10 | "Creates and returns a new authentication token for the currently authenticated User", 11 | { 12 | name: z.string().describe("Name of the token"), 13 | expiresAt: z.number().optional().describe("Token expiration timestamp"), 14 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 15 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 16 | }, 17 | async ({ name, expiresAt, teamId, slug }) => { 18 | const url = new URL(`${BASE_URL}/v3/user/tokens`); 19 | if (teamId) url.searchParams.append("teamId", teamId); 20 | if (slug) url.searchParams.append("slug", slug); 21 | 22 | const response = await fetch(url.toString(), { 23 | method: "POST", 24 | headers: { 25 | "Content-Type": "application/json", 26 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 27 | }, 28 | body: JSON.stringify({ name, expiresAt }), 29 | }); 30 | 31 | const data = await handleResponse(response); 32 | return { 33 | content: [{ type: "text", text: `Auth token created:\n${JSON.stringify(data, null, 2)}` }], 34 | }; 35 | } 36 | ); 37 | 38 | // Delete Auth Token 39 | server.tool( 40 | "delete_auth_token", 41 | "Invalidate an authentication token", 42 | { 43 | tokenId: z.string().describe("The identifier of the token to invalidate. Use 'current' for the current token"), 44 | }, 45 | async ({ tokenId }) => { 46 | const response = await fetch(`${BASE_URL}/v3/user/tokens/${tokenId}`, { 47 | method: "DELETE", 48 | headers: { 49 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 50 | }, 51 | }); 52 | 53 | const data = await handleResponse(response); 54 | return { 55 | content: [{ type: "text", text: `Auth token deleted:\n${JSON.stringify(data, null, 2)}` }], 56 | }; 57 | } 58 | ); 59 | 60 | // Get Auth Token Metadata 61 | server.tool( 62 | "get_auth_token", 63 | "Retrieve metadata about an authentication token", 64 | { 65 | tokenId: z.string().describe("The identifier of the token to retrieve. Use 'current' for the current token"), 66 | }, 67 | async ({ tokenId }) => { 68 | const response = await fetch(`${BASE_URL}/v5/user/tokens/${tokenId}`, { 69 | headers: { 70 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 71 | }, 72 | }); 73 | 74 | const data = await handleResponse(response); 75 | return { 76 | content: [{ type: "text", text: `Auth token metadata:\n${JSON.stringify(data, null, 2)}` }], 77 | }; 78 | } 79 | ); 80 | 81 | // List Auth Tokens 82 | server.tool( 83 | "list_auth_tokens", 84 | "Retrieve a list of the current User's authentication tokens", 85 | {}, 86 | async () => { 87 | const response = await fetch(`${BASE_URL}/v5/user/tokens`, { 88 | headers: { 89 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 90 | }, 91 | }); 92 | 93 | const data = await handleResponse(response); 94 | return { 95 | content: [{ type: "text", text: `Auth tokens list:\n${JSON.stringify(data, null, 2)}` }], 96 | }; 97 | } 98 | ); 99 | 100 | // SSO Token Exchange 101 | server.tool( 102 | "sso_token_exchange", 103 | "Exchange OAuth code for OIDC token during SSO authorization", 104 | { 105 | code: z.string().describe("The sensitive code received from Vercel"), 106 | state: z.string().optional().describe("The state received from the initialization request"), 107 | clientId: z.string().describe("The integration client id"), 108 | clientSecret: z.string().describe("The integration client secret"), 109 | redirectUri: z.string().optional().describe("The integration redirect URI") 110 | }, 111 | async ({ code, state, clientId, clientSecret, redirectUri }) => { 112 | const response = await fetch(`${BASE_URL}/v1/integrations/sso/token`, { 113 | method: "POST", 114 | headers: { 115 | "Content-Type": "application/json", 116 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 117 | }, 118 | body: JSON.stringify({ 119 | code, 120 | state, 121 | client_id: clientId, 122 | client_secret: clientSecret, 123 | redirect_uri: redirectUri 124 | }), 125 | }); 126 | 127 | const data = await handleResponse(response); 128 | return { 129 | content: [{ type: "text", text: `SSO token exchange completed:\n${JSON.stringify(data, null, 2)}` }], 130 | }; 131 | } 132 | ); 133 | } -------------------------------------------------------------------------------- /src/components/webhooks.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerWebhookTools(server: McpServer) { 7 | // Create a webhook 8 | server.tool( 9 | "create_webhook", 10 | "Creates a webhook", 11 | { 12 | url: z.string().url().regex(/^https?:\/\//).describe("The webhook URL"), 13 | events: z.array(z.enum([ 14 | "budget.reached", 15 | "budget.reset", 16 | "domain.created", 17 | "deployment.created", 18 | "deployment.error" 19 | ])).min(1).describe("Events to subscribe to"), 20 | projectIds: z.array(z.string()).min(1).max(50).describe("Project IDs to watch"), 21 | teamId: z.string().optional().describe("Team ID to perform the request on behalf of"), 22 | slug: z.string().optional().describe("Team slug to perform the request on behalf of") 23 | }, 24 | async ({ url, events, projectIds, teamId, slug }) => { 25 | const urlObj = new URL(`${BASE_URL}/v1/webhooks`); 26 | if (teamId) urlObj.searchParams.append("teamId", teamId); 27 | if (slug) urlObj.searchParams.append("slug", slug); 28 | 29 | const response = await fetch(urlObj.toString(), { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 34 | }, 35 | body: JSON.stringify({ 36 | url, 37 | events, 38 | projectIds 39 | }), 40 | }); 41 | 42 | const data = await handleResponse(response); 43 | return { 44 | content: [ 45 | { type: "text", text: `Webhook created:\n${JSON.stringify(data, null, 2)}` }, 46 | ], 47 | }; 48 | } 49 | ); 50 | 51 | // Delete a webhook 52 | server.tool( 53 | "delete_webhook", 54 | "Deletes a webhook", 55 | { 56 | id: z.string().describe("Webhook ID to delete"), 57 | teamId: z.string().optional().describe("Team ID to perform the request on behalf of"), 58 | slug: z.string().optional().describe("Team slug to perform the request on behalf of") 59 | }, 60 | async ({ id, teamId, slug }) => { 61 | const url = new URL(`${BASE_URL}/v1/webhooks/${id}`); 62 | if (teamId) url.searchParams.append("teamId", teamId); 63 | if (slug) url.searchParams.append("slug", slug); 64 | 65 | const response = await fetch(url.toString(), { 66 | method: "DELETE", 67 | headers: { 68 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 69 | }, 70 | }); 71 | 72 | if (response.status === 204) { 73 | return { 74 | content: [ 75 | { type: "text", text: `Webhook ${id} was successfully deleted` }, 76 | ], 77 | }; 78 | } 79 | 80 | const errorData = await response.text(); 81 | throw new Error(`Failed to delete webhook: ${response.status} - ${errorData}`); 82 | } 83 | ); 84 | 85 | // List webhooks 86 | server.tool( 87 | "list_webhooks", 88 | "Get a list of webhooks", 89 | { 90 | projectId: z.string().regex(/^[a-zA-z0-9_]+$/).optional().describe("Filter by project ID"), 91 | teamId: z.string().optional().describe("Team ID to perform the request on behalf of"), 92 | slug: z.string().optional().describe("Team slug to perform the request on behalf of") 93 | }, 94 | async ({ projectId, teamId, slug }) => { 95 | const url = new URL(`${BASE_URL}/v1/webhooks`); 96 | if (projectId) url.searchParams.append("projectId", projectId); 97 | if (teamId) url.searchParams.append("teamId", teamId); 98 | if (slug) url.searchParams.append("slug", slug); 99 | 100 | const response = await fetch(url.toString(), { 101 | headers: { 102 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 103 | }, 104 | }); 105 | 106 | const data = await handleResponse(response); 107 | return { 108 | content: [ 109 | { type: "text", text: `Webhooks:\n${JSON.stringify(data, null, 2)}` }, 110 | ], 111 | }; 112 | } 113 | ); 114 | 115 | // Get a webhook 116 | server.tool( 117 | "get_webhook", 118 | "Get a webhook", 119 | { 120 | id: z.string().describe("Webhook ID"), 121 | teamId: z.string().optional().describe("Team ID to perform the request on behalf of"), 122 | slug: z.string().optional().describe("Team slug to perform the request on behalf of") 123 | }, 124 | async ({ id, teamId, slug }) => { 125 | const url = new URL(`${BASE_URL}/v1/webhooks/${id}`); 126 | if (teamId) url.searchParams.append("teamId", teamId); 127 | if (slug) url.searchParams.append("slug", slug); 128 | 129 | const response = await fetch(url.toString(), { 130 | headers: { 131 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 132 | }, 133 | }); 134 | 135 | const data = await handleResponse(response); 136 | return { 137 | content: [ 138 | { type: "text", text: `Webhook details:\n${JSON.stringify(data, null, 2)}` }, 139 | ], 140 | }; 141 | } 142 | ); 143 | } -------------------------------------------------------------------------------- /src/components/secrets.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | // Common parameter schemas 7 | const teamParams = { 8 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 9 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 10 | }; 11 | 12 | export function registerSecretTools(server: McpServer) { 13 | // Create a new secret 14 | server.tool( 15 | "create_secret", 16 | "Create a new secret", 17 | { 18 | name: z.string().max(100).describe("The name of the secret (max 100 characters)"), 19 | value: z.string().describe("The value of the new secret"), 20 | decryptable: z.boolean().optional().describe("Whether the secret value can be decrypted after creation"), 21 | projectId: z.string().optional().describe("Associate a secret to a project"), 22 | ...teamParams 23 | }, 24 | async ({ name, value, decryptable, projectId, teamId, slug }) => { 25 | const url = new URL(`${BASE_URL}/v2/secrets/${name}`); 26 | if (teamId) url.searchParams.append("teamId", teamId); 27 | if (slug) url.searchParams.append("slug", slug); 28 | 29 | const response = await fetch(url.toString(), { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 34 | }, 35 | body: JSON.stringify({ name, value, decryptable, projectId }), 36 | }); 37 | 38 | const data = await handleResponse(response); 39 | return { 40 | content: [ 41 | { type: "text", text: `Secret created:\n${JSON.stringify(data, null, 2)}` }, 42 | ], 43 | }; 44 | } 45 | ); 46 | 47 | // Change secret name 48 | server.tool( 49 | "update_secret_name", 50 | "Change the name of a secret", 51 | { 52 | currentName: z.string().describe("The current name of the secret"), 53 | newName: z.string().max(100).describe("The new name for the secret"), 54 | ...teamParams 55 | }, 56 | async ({ currentName, newName, teamId, slug }) => { 57 | const url = new URL(`${BASE_URL}/v2/secrets/${currentName}`); 58 | if (teamId) url.searchParams.append("teamId", teamId); 59 | if (slug) url.searchParams.append("slug", slug); 60 | 61 | const response = await fetch(url.toString(), { 62 | method: "PATCH", 63 | headers: { 64 | "Content-Type": "application/json", 65 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 66 | }, 67 | body: JSON.stringify({ name: newName }), 68 | }); 69 | 70 | const data = await handleResponse(response); 71 | return { 72 | content: [ 73 | { type: "text", text: `Secret name updated:\n${JSON.stringify(data, null, 2)}` }, 74 | ], 75 | }; 76 | } 77 | ); 78 | 79 | // Delete a secret 80 | server.tool( 81 | "delete_secret", 82 | "Delete a secret", 83 | { 84 | idOrName: z.string().describe("The name or unique identifier of the secret"), 85 | ...teamParams 86 | }, 87 | async ({ idOrName, teamId, slug }) => { 88 | const url = new URL(`${BASE_URL}/v2/secrets/${idOrName}`); 89 | if (teamId) url.searchParams.append("teamId", teamId); 90 | if (slug) url.searchParams.append("slug", slug); 91 | 92 | const response = await fetch(url.toString(), { 93 | method: "DELETE", 94 | headers: { 95 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 96 | }, 97 | }); 98 | 99 | const data = await handleResponse(response); 100 | return { 101 | content: [ 102 | { type: "text", text: `Secret deleted:\n${JSON.stringify(data, null, 2)}` }, 103 | ], 104 | }; 105 | } 106 | ); 107 | 108 | // Get a single secret 109 | server.tool( 110 | "get_secret", 111 | "Get information for a specific secret", 112 | { 113 | idOrName: z.string().describe("The name or unique identifier of the secret"), 114 | decrypt: z.enum(["true", "false"]).optional().describe("Whether to try to decrypt the value of the secret"), 115 | ...teamParams 116 | }, 117 | async ({ idOrName, decrypt, teamId, slug }) => { 118 | const url = new URL(`${BASE_URL}/v3/secrets/${idOrName}`); 119 | if (decrypt) url.searchParams.append("decrypt", decrypt); 120 | if (teamId) url.searchParams.append("teamId", teamId); 121 | if (slug) url.searchParams.append("slug", slug); 122 | 123 | const response = await fetch(url.toString(), { 124 | headers: { 125 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 126 | }, 127 | }); 128 | 129 | const data = await handleResponse(response); 130 | return { 131 | content: [ 132 | { type: "text", text: `Secret information:\n${JSON.stringify(data, null, 2)}` }, 133 | ], 134 | }; 135 | } 136 | ); 137 | 138 | // List secrets 139 | server.tool( 140 | "list_secrets", 141 | "List all secrets", 142 | { 143 | id: z.string().optional().describe("Filter by comma separated secret ids"), 144 | projectId: z.string().optional().describe("Filter by project ID"), 145 | ...teamParams 146 | }, 147 | async ({ id, projectId, teamId, slug }) => { 148 | const url = new URL(`${BASE_URL}/v3/secrets`); 149 | if (id) url.searchParams.append("id", id); 150 | if (projectId) url.searchParams.append("projectId", projectId); 151 | if (teamId) url.searchParams.append("teamId", teamId); 152 | if (slug) url.searchParams.append("slug", slug); 153 | 154 | const response = await fetch(url.toString(), { 155 | headers: { 156 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 157 | }, 158 | }); 159 | 160 | const data = await handleResponse(response); 161 | return { 162 | content: [ 163 | { type: "text", text: `Secrets:\n${JSON.stringify(data, null, 2)}` }, 164 | ], 165 | }; 166 | } 167 | ); 168 | } -------------------------------------------------------------------------------- /src/components/dns.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerDnsTools(server: McpServer) { 7 | // Create DNS Record 8 | server.tool( 9 | "create_dns_record", 10 | "Creates a DNS record for a domain", 11 | { 12 | domain: z.string().describe("The domain used to create the DNS record"), 13 | type: z.enum(["A", "AAAA", "ALIAS", "CAA", "CNAME"]).describe("The type of record"), 14 | name: z.string().describe("The name of the DNS record"), 15 | value: z.string().describe("The value of the DNS record"), 16 | ttl: z.number().min(60).max(2147483647).optional().describe("The Time to live (TTL) value"), 17 | comment: z.string().max(500).optional().describe("A comment to add context"), 18 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 19 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 20 | }, 21 | async ({ domain, type, name, value, ttl, comment, teamId, slug }) => { 22 | const url = new URL(`${BASE_URL}/v2/domains/${domain}/records`); 23 | if (teamId) url.searchParams.append("teamId", teamId); 24 | if (slug) url.searchParams.append("slug", slug); 25 | 26 | const response = await fetch(url.toString(), { 27 | method: "POST", 28 | headers: { 29 | "Content-Type": "application/json", 30 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 31 | }, 32 | body: JSON.stringify({ type, name, value, ttl, comment }), 33 | }); 34 | 35 | const data = await handleResponse(response); 36 | return { 37 | content: [{ type: "text", text: `DNS record created:\n${JSON.stringify(data, null, 2)}` }], 38 | }; 39 | } 40 | ); 41 | 42 | // Delete DNS Record 43 | server.tool( 44 | "delete_dns_record", 45 | "Removes an existing DNS record from a domain name", 46 | { 47 | domain: z.string().describe("The domain name"), 48 | recordId: z.string().describe("The DNS record ID"), 49 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 50 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 51 | }, 52 | async ({ domain, recordId, teamId, slug }) => { 53 | const url = new URL(`${BASE_URL}/v2/domains/${domain}/records/${recordId}`); 54 | if (teamId) url.searchParams.append("teamId", teamId); 55 | if (slug) url.searchParams.append("slug", slug); 56 | 57 | const response = await fetch(url.toString(), { 58 | method: "DELETE", 59 | headers: { 60 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 61 | }, 62 | }); 63 | 64 | const data = await handleResponse(response); 65 | return { 66 | content: [{ type: "text", text: `DNS record deleted:\n${JSON.stringify(data, null, 2)}` }], 67 | }; 68 | } 69 | ); 70 | 71 | // List DNS Records 72 | server.tool( 73 | "list_dns_records", 74 | "Retrieves a list of DNS records created for a domain name", 75 | { 76 | domain: z.string().describe("The domain name"), 77 | limit: z.string().optional().describe("Maximum number of records to list"), 78 | since: z.string().optional().describe("Get records created after this JavaScript timestamp"), 79 | until: z.string().optional().describe("Get records created before this JavaScript timestamp"), 80 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 81 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 82 | }, 83 | async ({ domain, limit, since, until, teamId, slug }) => { 84 | const url = new URL(`${BASE_URL}/v4/domains/${domain}/records`); 85 | if (limit) url.searchParams.append("limit", limit); 86 | if (since) url.searchParams.append("since", since); 87 | if (until) url.searchParams.append("until", until); 88 | if (teamId) url.searchParams.append("teamId", teamId); 89 | if (slug) url.searchParams.append("slug", slug); 90 | 91 | const response = await fetch(url.toString(), { 92 | headers: { 93 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 94 | }, 95 | }); 96 | 97 | const data = await handleResponse(response); 98 | return { 99 | content: [{ type: "text", text: `DNS records:\n${JSON.stringify(data, null, 2)}` }], 100 | }; 101 | } 102 | ); 103 | 104 | // Update DNS Record 105 | server.tool( 106 | "update_dns_record", 107 | "Updates an existing DNS record for a domain name", 108 | { 109 | recordId: z.string().describe("The id of the DNS record"), 110 | name: z.string().nullable().optional().describe("The name of the DNS record"), 111 | value: z.string().nullable().optional().describe("The value of the DNS record"), 112 | type: z.enum(["A", "AAAA", "ALIAS", "CAA", "CNAME"]).nullable().optional().describe("The type of the DNS record"), 113 | ttl: z.number().min(60).max(2147483647).nullable().optional().describe("The Time to live (TTL) value"), 114 | mxPriority: z.number().nullable().optional().describe("The MX priority value"), 115 | comment: z.string().max(500).optional().describe("A comment to add context"), 116 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 117 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 118 | }, 119 | async ({ recordId, name, value, type, ttl, mxPriority, comment, teamId, slug }) => { 120 | const url = new URL(`${BASE_URL}/v1/domains/records/${recordId}`); 121 | if (teamId) url.searchParams.append("teamId", teamId); 122 | if (slug) url.searchParams.append("slug", slug); 123 | 124 | const response = await fetch(url.toString(), { 125 | method: "PATCH", 126 | headers: { 127 | "Content-Type": "application/json", 128 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 129 | }, 130 | body: JSON.stringify({ 131 | name, 132 | value, 133 | type, 134 | ttl, 135 | mxPriority, 136 | comment, 137 | }), 138 | }); 139 | 140 | const data = await handleResponse(response); 141 | return { 142 | content: [{ type: "text", text: `DNS record updated:\n${JSON.stringify(data, null, 2)}` }], 143 | }; 144 | } 145 | ); 146 | } -------------------------------------------------------------------------------- /src/components/aliases.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerAliasTools(server: McpServer) { 7 | // Assign an Alias 8 | server.tool( 9 | "assign_alias", 10 | "Creates a new alias for the deployment with the given deployment ID", 11 | { 12 | id: z.string().describe("The ID of the deployment to assign the alias to"), 13 | alias: z.string().describe("The alias to assign (e.g. my-alias.vercel.app)"), 14 | redirect: z.string().nullable().optional().describe("Optional hostname to redirect to using 307"), 15 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 16 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 17 | }, 18 | async ({ id, alias, redirect, teamId, slug }) => { 19 | const url = new URL(`${BASE_URL}/v2/deployments/${id}/aliases`); 20 | if (teamId) url.searchParams.append("teamId", teamId); 21 | if (slug) url.searchParams.append("slug", slug); 22 | 23 | const response = await fetch(url.toString(), { 24 | method: "POST", 25 | headers: { 26 | "Content-Type": "application/json", 27 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 28 | }, 29 | body: JSON.stringify({ alias, redirect }), 30 | }); 31 | 32 | const data = await handleResponse(response); 33 | return { 34 | content: [{ type: "text", text: `Alias assigned:\n${JSON.stringify(data, null, 2)}` }], 35 | }; 36 | } 37 | ); 38 | 39 | // Delete an Alias 40 | server.tool( 41 | "delete_alias", 42 | "Delete an Alias with the specified ID", 43 | { 44 | aliasId: z.string().describe("The ID or alias that will be removed"), 45 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 46 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 47 | }, 48 | async ({ aliasId, teamId, slug }) => { 49 | const url = new URL(`${BASE_URL}/v2/aliases/${aliasId}`); 50 | if (teamId) url.searchParams.append("teamId", teamId); 51 | if (slug) url.searchParams.append("slug", slug); 52 | 53 | const response = await fetch(url.toString(), { 54 | method: "DELETE", 55 | headers: { 56 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 57 | }, 58 | }); 59 | 60 | const data = await handleResponse(response); 61 | return { 62 | content: [{ type: "text", text: `Alias deleted:\n${JSON.stringify(data, null, 2)}` }], 63 | }; 64 | } 65 | ); 66 | 67 | // Get an Alias 68 | server.tool( 69 | "get_alias", 70 | "Retrieves an Alias for the given host name or alias ID", 71 | { 72 | idOrAlias: z.string().describe("The alias or alias ID to be retrieved"), 73 | projectId: z.string().optional().describe("Get the alias only if it is assigned to the provided project ID"), 74 | since: z.number().optional().describe("Get the alias only if it was created after this JavaScript timestamp"), 75 | until: z.number().optional().describe("Get the alias only if it was created before this JavaScript timestamp"), 76 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 77 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 78 | }, 79 | async ({ idOrAlias, projectId, since, until, teamId, slug }) => { 80 | const url = new URL(`${BASE_URL}/v4/aliases/${idOrAlias}`); 81 | if (projectId) url.searchParams.append("projectId", projectId); 82 | if (since) url.searchParams.append("since", since.toString()); 83 | if (until) url.searchParams.append("until", until.toString()); 84 | if (teamId) url.searchParams.append("teamId", teamId); 85 | if (slug) url.searchParams.append("slug", slug); 86 | 87 | const response = await fetch(url.toString(), { 88 | headers: { 89 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 90 | }, 91 | }); 92 | 93 | const data = await handleResponse(response); 94 | return { 95 | content: [{ type: "text", text: `Alias information:\n${JSON.stringify(data, null, 2)}` }], 96 | }; 97 | } 98 | ); 99 | 100 | // List Aliases 101 | server.tool( 102 | "list_aliases", 103 | "Retrieves a list of aliases for the authenticated User or Team", 104 | { 105 | domain: z.string().optional().describe("Get only aliases of the given domain name"), 106 | limit: z.number().optional().describe("Maximum number of aliases to list from a request"), 107 | projectId: z.string().optional().describe("Filter aliases from the given projectId"), 108 | since: z.number().optional().describe("Get aliases created after this JavaScript timestamp"), 109 | until: z.number().optional().describe("Get aliases created before this JavaScript timestamp"), 110 | rollbackDeploymentId: z.string().optional().describe("Get aliases that would be rolled back for the given deployment"), 111 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 112 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 113 | }, 114 | async ({ domain, limit, projectId, since, until, rollbackDeploymentId, teamId, slug }) => { 115 | const url = new URL(`${BASE_URL}/v4/aliases`); 116 | if (domain) url.searchParams.append("domain", domain); 117 | if (limit) url.searchParams.append("limit", limit.toString()); 118 | if (projectId) url.searchParams.append("projectId", projectId); 119 | if (since) url.searchParams.append("since", since.toString()); 120 | if (until) url.searchParams.append("until", until.toString()); 121 | if (rollbackDeploymentId) url.searchParams.append("rollbackDeploymentId", rollbackDeploymentId); 122 | if (teamId) url.searchParams.append("teamId", teamId); 123 | if (slug) url.searchParams.append("slug", slug); 124 | 125 | const response = await fetch(url.toString(), { 126 | headers: { 127 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 128 | }, 129 | }); 130 | 131 | const data = await handleResponse(response); 132 | return { 133 | content: [{ type: "text", text: `Aliases list:\n${JSON.stringify(data, null, 2)}` }], 134 | }; 135 | } 136 | ); 137 | 138 | // List Deployment Aliases 139 | server.tool( 140 | "list_deployment_aliases", 141 | "Retrieves all Aliases for the Deployment with the given ID", 142 | { 143 | id: z.string().describe("The ID of the deployment the aliases should be listed for"), 144 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 145 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 146 | }, 147 | async ({ id, teamId, slug }) => { 148 | const url = new URL(`${BASE_URL}/v2/deployments/${id}/aliases`); 149 | if (teamId) url.searchParams.append("teamId", teamId); 150 | if (slug) url.searchParams.append("slug", slug); 151 | 152 | const response = await fetch(url.toString(), { 153 | headers: { 154 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 155 | }, 156 | }); 157 | 158 | const data = await handleResponse(response); 159 | return { 160 | content: [{ type: "text", text: `Deployment aliases:\n${JSON.stringify(data, null, 2)}` }], 161 | }; 162 | } 163 | ); 164 | } -------------------------------------------------------------------------------- /src/tool-manager.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | 3 | interface GroupUsage { 4 | lastUsed: number; 5 | tools: string[]; 6 | } 7 | 8 | interface ToolGroupMap { 9 | [key: string]: string; 10 | } 11 | 12 | export class ToolManager { 13 | private server: McpServer; 14 | private activeGroups: Map = new Map(); 15 | private readonly MAX_ACTIVE_GROUPS = 2; 16 | private readonly toolToGroupMap: ToolGroupMap = { 17 | // Infrastructure tools 18 | 'edge_config': 'infrastructure', 19 | 'secret': 'infrastructure', 20 | 'env': 'infrastructure', 21 | 'webhook': 'infrastructure', 22 | 'logdrain': 'infrastructure', 23 | 'speed_insights': 'infrastructure', 24 | 'firewall': 'infrastructure', 25 | 26 | // Access tools 27 | 'user': 'access', 28 | 'team': 'access', 29 | 'auth': 'access', 30 | 'access_group': 'access', 31 | 'security': 'access', 32 | 33 | // Project tools 34 | 'project': 'projects', 35 | 'list_projects': 'projects', 36 | 'projects': 'projects', 37 | 'deployment': 'projects', 38 | 'member': 'projects', 39 | 'transfer': 'projects', 40 | 'show_projects': 'projects', 41 | 'get_projects': 'projects', 42 | 'view_projects': 'projects', 43 | 'display_projects': 'projects', 44 | 'fetch_projects': 'projects', 45 | 'retrieve_projects': 'projects', 46 | 47 | // Domain tools 48 | 'domain': 'domains', 49 | 'dns': 'domains', 50 | 'cert': 'domains', 51 | 'alias': 'domains', 52 | 53 | // Integration tools 54 | 'integration': 'integrations', 55 | 'marketplace': 'integrations', 56 | 'artifact': 'integrations' 57 | }; 58 | 59 | constructor(server: McpServer) { 60 | this.server = server; 61 | } 62 | 63 | async loadGroup(groupName: string) { 64 | if (this.activeGroups.has(groupName)) { 65 | // Update last used timestamp 66 | this.activeGroups.get(groupName)!.lastUsed = Date.now(); 67 | return; 68 | } 69 | 70 | // Always unload the oldest group if we're at max capacity 71 | if (this.activeGroups.size >= this.MAX_ACTIVE_GROUPS) { 72 | const oldestGroup = this.getLeastRecentlyUsedGroup(); 73 | await this.unloadGroup(oldestGroup); 74 | } 75 | 76 | try { 77 | const group = await import(`./tool-groups/${groupName}/index.js`); 78 | const tools = await group.register(this.server); 79 | 80 | this.activeGroups.set(groupName, { 81 | lastUsed: Date.now(), 82 | tools 83 | }); 84 | 85 | console.log(`Loaded tool group: ${groupName}`); 86 | } catch (error) { 87 | console.error(`Failed to load tool group ${groupName}:`, error); 88 | } 89 | } 90 | 91 | async unloadGroup(groupName: string) { 92 | const group = this.activeGroups.get(groupName); 93 | if (!group) return; 94 | 95 | try { 96 | // Simply remove the group from tracking 97 | // The tools will be overwritten when new groups are loaded 98 | this.activeGroups.delete(groupName); 99 | console.log(`Unloaded tool group: ${groupName}`); 100 | } catch (error) { 101 | console.error(`Failed to unload tool group ${groupName}:`, error); 102 | } 103 | } 104 | 105 | private getLeastRecentlyUsedGroup(): string { 106 | let oldestTime = Infinity; 107 | let oldestGroup = ''; 108 | 109 | for (const [group, usage] of this.activeGroups.entries()) { 110 | if (usage.lastUsed < oldestTime) { 111 | oldestTime = usage.lastUsed; 112 | oldestGroup = group; 113 | } 114 | } 115 | 116 | return oldestGroup; 117 | } 118 | 119 | getActiveGroups(): string[] { 120 | return Array.from(this.activeGroups.keys()); 121 | } 122 | 123 | private findGroupForTool(toolName: string): string { 124 | // First check direct tool name matches 125 | for (const [tool, group] of Object.entries(this.toolToGroupMap)) { 126 | if (toolName.includes(tool)) { 127 | return group; 128 | } 129 | } 130 | return ''; 131 | } 132 | 133 | async suggestAndLoadGroups(query: string) { 134 | const normalizedQuery = query.toLowerCase(); 135 | let groupToLoad = ''; 136 | 137 | // First try to find group by exact tool name 138 | const words = normalizedQuery.split(/[\s_-]+/); 139 | for (const word of words) { 140 | groupToLoad = this.findGroupForTool(word); 141 | if (groupToLoad) break; 142 | } 143 | 144 | // If no group found by tool name, try keyword matching 145 | if (!groupToLoad) { 146 | if (normalizedQuery.includes('edge') || 147 | normalizedQuery.includes('secret') || 148 | normalizedQuery.includes('env') || 149 | normalizedQuery.includes('environment') || 150 | normalizedQuery.includes('webhook') || 151 | normalizedQuery.includes('log') || 152 | normalizedQuery.includes('speed') || 153 | normalizedQuery.includes('vitals')) { 154 | groupToLoad = 'infrastructure'; 155 | } else if (normalizedQuery.includes('user') || 156 | normalizedQuery.includes('team') || 157 | normalizedQuery.includes('auth') || 158 | normalizedQuery.includes('access') || 159 | normalizedQuery.includes('firewall') || 160 | normalizedQuery.includes('security')) { 161 | groupToLoad = 'access'; 162 | } else if (normalizedQuery.includes('project') || 163 | normalizedQuery.includes('projects') || 164 | normalizedQuery.includes('list project') || 165 | normalizedQuery.includes('list projects') || 166 | normalizedQuery.includes('show project') || 167 | normalizedQuery.includes('show projects') || 168 | normalizedQuery.includes('get project') || 169 | normalizedQuery.includes('get projects') || 170 | normalizedQuery.includes('view project') || 171 | normalizedQuery.includes('view projects') || 172 | normalizedQuery.includes('display project') || 173 | normalizedQuery.includes('display projects') || 174 | normalizedQuery.includes('fetch project') || 175 | normalizedQuery.includes('fetch projects') || 176 | normalizedQuery.includes('retrieve project') || 177 | normalizedQuery.includes('retrieve projects') || 178 | normalizedQuery.includes('deploy') || 179 | normalizedQuery.includes('member') || 180 | normalizedQuery.includes('transfer') || 181 | normalizedQuery.includes('file')) { 182 | groupToLoad = 'projects'; 183 | } else if (normalizedQuery.includes('domain') || 184 | normalizedQuery.includes('dns') || 185 | normalizedQuery.includes('cert') || 186 | normalizedQuery.includes('ssl') || 187 | normalizedQuery.includes('tls') || 188 | normalizedQuery.includes('alias')) { 189 | groupToLoad = 'domains'; 190 | } else if (normalizedQuery.includes('integration') || 191 | normalizedQuery.includes('marketplace') || 192 | normalizedQuery.includes('artifact') || 193 | normalizedQuery.includes('int_')) { 194 | groupToLoad = 'integrations'; 195 | } 196 | } 197 | 198 | // Load the group if one was found 199 | if (groupToLoad) { 200 | await this.loadGroup(groupToLoad); 201 | } 202 | } 203 | 204 | // New method to get available tools in a group 205 | async getGroupTools(groupName: string): Promise { 206 | try { 207 | const group = await import(`./tool-groups/${groupName}/index.js`); 208 | return group.register(this.server); 209 | } catch (error) { 210 | console.error(`Failed to get tools for group ${groupName}:`, error); 211 | return []; 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /src/components/integrations.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerIntegrationTools(server: McpServer) { 7 | // Delete integration configuration 8 | server.tool( 9 | "int_delete", 10 | "Delete an integration configuration", 11 | { 12 | id: z.string().describe("ID of the configuration to delete"), 13 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 14 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 15 | }, 16 | async ({ id, teamId, slug }) => { 17 | const url = new URL(`${BASE_URL}/v1/integrations/configuration/${id}`); 18 | if (teamId) url.searchParams.append("teamId", teamId); 19 | if (slug) url.searchParams.append("slug", slug); 20 | 21 | const response = await fetch(url.toString(), { 22 | method: "DELETE", 23 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 24 | }); 25 | 26 | await handleResponse(response); 27 | return { 28 | content: [{ type: "text", text: "Integration configuration deleted successfully" }] 29 | }; 30 | } 31 | ); 32 | 33 | // List configurations 34 | server.tool( 35 | "int_list", 36 | "Get configurations for the authenticated user or team", 37 | { 38 | view: z.enum(["account", "project"]).describe("View type for configurations"), 39 | installationType: z.enum(["marketplace", "external"]).optional().describe("Type of installation"), 40 | integrationIdOrSlug: z.string().optional().describe("ID of the integration"), 41 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 42 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 43 | }, 44 | async ({ view, installationType, integrationIdOrSlug, teamId, slug }) => { 45 | const url = new URL(`${BASE_URL}/v1/integrations/configurations`); 46 | url.searchParams.append("view", view); 47 | if (installationType) url.searchParams.append("installationType", installationType); 48 | if (integrationIdOrSlug) url.searchParams.append("integrationIdOrSlug", integrationIdOrSlug); 49 | if (teamId) url.searchParams.append("teamId", teamId); 50 | if (slug) url.searchParams.append("slug", slug); 51 | 52 | const response = await fetch(url.toString(), { 53 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 54 | }); 55 | 56 | const data = await handleResponse(response); 57 | return { 58 | content: [{ type: "text", text: `Integration configurations:\n${JSON.stringify(data, null, 2)}` }] 59 | }; 60 | } 61 | ); 62 | 63 | // List git namespaces 64 | server.tool( 65 | "int_gitns", 66 | "List git namespaces by provider", 67 | { 68 | host: z.string().optional().describe("The custom Git host if using a custom Git provider"), 69 | provider: z.enum(["github", "github-custom-host", "gitlab", "bitbucket"]).optional().describe("Git provider") 70 | }, 71 | async ({ host, provider }) => { 72 | const url = new URL(`${BASE_URL}/v1/integrations/git-namespaces`); 73 | if (host) url.searchParams.append("host", host); 74 | if (provider) url.searchParams.append("provider", provider); 75 | 76 | const response = await fetch(url.toString(), { 77 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 78 | }); 79 | 80 | const data = await handleResponse(response); 81 | return { 82 | content: [{ type: "text", text: `Git namespaces:\n${JSON.stringify(data, null, 2)}` }] 83 | }; 84 | } 85 | ); 86 | 87 | // Search repositories 88 | server.tool( 89 | "int_search_repo", 90 | "List git repositories linked to namespace", 91 | { 92 | query: z.string().optional().describe("Search query"), 93 | namespaceId: z.string().optional().describe("Namespace ID"), 94 | provider: z.enum(["github", "github-custom-host", "gitlab", "bitbucket"]).optional().describe("Git provider"), 95 | installationId: z.string().optional().describe("Installation ID"), 96 | host: z.string().optional().describe("Custom Git host"), 97 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 98 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 99 | }, 100 | async ({ query, namespaceId, provider, installationId, host, teamId, slug }) => { 101 | const url = new URL(`${BASE_URL}/v1/integrations/search-repo`); 102 | if (query) url.searchParams.append("query", query); 103 | if (namespaceId) url.searchParams.append("namespaceId", namespaceId); 104 | if (provider) url.searchParams.append("provider", provider); 105 | if (installationId) url.searchParams.append("installationId", installationId); 106 | if (host) url.searchParams.append("host", host); 107 | if (teamId) url.searchParams.append("teamId", teamId); 108 | if (slug) url.searchParams.append("slug", slug); 109 | 110 | const response = await fetch(url.toString(), { 111 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 112 | }); 113 | 114 | const data = await handleResponse(response); 115 | return { 116 | content: [{ type: "text", text: `Git repositories:\n${JSON.stringify(data, null, 2)}` }] 117 | }; 118 | } 119 | ); 120 | 121 | // Get integration configuration 122 | server.tool( 123 | "int_get", 124 | "Retrieve an integration configuration", 125 | { 126 | id: z.string().describe("ID of the configuration to check"), 127 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 128 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 129 | }, 130 | async ({ id, teamId, slug }) => { 131 | const url = new URL(`${BASE_URL}/v1/integrations/configuration/${id}`); 132 | if (teamId) url.searchParams.append("teamId", teamId); 133 | if (slug) url.searchParams.append("slug", slug); 134 | 135 | const response = await fetch(url.toString(), { 136 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 137 | }); 138 | 139 | const data = await handleResponse(response); 140 | return { 141 | content: [{ type: "text", text: `Integration configuration:\n${JSON.stringify(data, null, 2)}` }] 142 | }; 143 | } 144 | ); 145 | 146 | // Update deployment integration action 147 | server.tool( 148 | "int_update_action", 149 | "Update deployment integration action", 150 | { 151 | deploymentId: z.string().describe("Deployment ID"), 152 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 153 | resourceId: z.string().describe("Resource ID"), 154 | action: z.string().describe("Action to update"), 155 | status: z.enum(["running", "succeeded", "failed"]).describe("Status of the action"), 156 | statusText: z.string().optional().describe("Status text"), 157 | outcomes: z.array(z.object({ 158 | kind: z.string(), 159 | secrets: z.array(z.object({ 160 | name: z.string(), 161 | value: z.string() 162 | })).optional() 163 | })).optional().describe("Action outcomes") 164 | }, 165 | async ({ deploymentId, integrationConfigurationId, resourceId, action, status, statusText, outcomes }) => { 166 | const url = new URL(`${BASE_URL}/v1/deployments/${deploymentId}/integrations/${integrationConfigurationId}/resources/${resourceId}/actions/${action}`); 167 | 168 | const response = await fetch(url.toString(), { 169 | method: "PATCH", 170 | headers: { 171 | "Content-Type": "application/json", 172 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` 173 | }, 174 | body: JSON.stringify({ 175 | status, 176 | statusText, 177 | outcomes 178 | }) 179 | }); 180 | 181 | await handleResponse(response); 182 | return { 183 | content: [{ type: "text", text: "Integration action updated successfully" }] 184 | }; 185 | } 186 | ); 187 | } -------------------------------------------------------------------------------- /src/components/environments.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | // Types for environment management 7 | const BranchMatcherSchema = z.object({ 8 | type: z.enum(['equals']), 9 | pattern: z.string() 10 | }).optional(); 11 | 12 | const CreateEnvironmentSchema = z.object({ 13 | slug: z.string().max(32), 14 | description: z.string().max(256).optional(), 15 | branchMatcher: BranchMatcherSchema, 16 | copyEnvVarsFrom: z.string().optional() 17 | }); 18 | 19 | const UpdateEnvironmentSchema = z.object({ 20 | slug: z.string().max(32).optional(), 21 | description: z.string().max(256).optional(), 22 | branchMatcher: BranchMatcherSchema.nullable() 23 | }); 24 | 25 | const DeleteEnvironmentSchema = z.object({ 26 | deleteUnassignedEnvironmentVariables: z.boolean().optional() 27 | }); 28 | 29 | export function registerEnvironmentTools(server: McpServer) { 30 | // Create Environment 31 | server.tool( 32 | "create_environment", 33 | "Create a custom environment for a project", 34 | { 35 | idOrName: z.string().describe("Project ID or name"), 36 | slug: z.string().max(32).describe("Environment slug"), 37 | description: z.string().max(256).optional().describe("Environment description"), 38 | branchMatcher: z.object({ 39 | type: z.enum(["equals"]), 40 | pattern: z.string() 41 | }).optional().describe("Branch matching configuration"), 42 | copyEnvVarsFrom: z.string().optional().describe("Copy environment variables from this environment"), 43 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 44 | teamSlug: z.string().optional().describe("The Team slug to perform the request on behalf of") 45 | }, 46 | async ({ idOrName, slug, description, branchMatcher, copyEnvVarsFrom, teamId, teamSlug }) => { 47 | const url = new URL(`${BASE_URL}/v9/projects/${idOrName}/custom-environments`); 48 | if (teamId) url.searchParams.append("teamId", teamId); 49 | if (teamSlug) url.searchParams.append("slug", teamSlug); 50 | 51 | const response = await fetch(url.toString(), { 52 | method: "POST", 53 | headers: { 54 | "Content-Type": "application/json", 55 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 56 | }, 57 | body: JSON.stringify({ slug, description, branchMatcher, copyEnvVarsFrom }), 58 | }); 59 | 60 | const data = await handleResponse(response); 61 | return { 62 | content: [{ type: "text", text: `Environment created:\n${JSON.stringify(data, null, 2)}` }], 63 | }; 64 | } 65 | ); 66 | 67 | // Delete Environment 68 | server.tool( 69 | "delete_environment", 70 | "Remove a custom environment from a project", 71 | { 72 | idOrName: z.string().describe("Project ID or name"), 73 | environmentSlugOrId: z.string().describe("Environment slug or ID"), 74 | deleteUnassignedEnvVars: z.boolean().optional().describe("Delete unassigned environment variables"), 75 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 76 | teamSlug: z.string().optional().describe("The Team slug to perform the request on behalf of") 77 | }, 78 | async ({ idOrName, environmentSlugOrId, deleteUnassignedEnvVars, teamId, teamSlug }) => { 79 | const url = new URL(`${BASE_URL}/v9/projects/${idOrName}/custom-environments/${environmentSlugOrId}`); 80 | if (teamId) url.searchParams.append("teamId", teamId); 81 | if (teamSlug) url.searchParams.append("slug", teamSlug); 82 | 83 | const response = await fetch(url.toString(), { 84 | method: "DELETE", 85 | headers: { 86 | "Content-Type": "application/json", 87 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 88 | }, 89 | body: JSON.stringify({ deleteUnassignedEnvironmentVariables: deleteUnassignedEnvVars }), 90 | }); 91 | 92 | const data = await handleResponse(response); 93 | return { 94 | content: [{ type: "text", text: `Environment deleted:\n${JSON.stringify(data, null, 2)}` }], 95 | }; 96 | } 97 | ); 98 | 99 | // Get Environment 100 | server.tool( 101 | "get_environment", 102 | "Retrieve a custom environment", 103 | { 104 | idOrName: z.string().describe("Project ID or name"), 105 | environmentSlugOrId: z.string().describe("Environment slug or ID"), 106 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 107 | teamSlug: z.string().optional().describe("The Team slug to perform the request on behalf of") 108 | }, 109 | async ({ idOrName, environmentSlugOrId, teamId, teamSlug }) => { 110 | const url = new URL(`${BASE_URL}/v9/projects/${idOrName}/custom-environments/${environmentSlugOrId}`); 111 | if (teamId) url.searchParams.append("teamId", teamId); 112 | if (teamSlug) url.searchParams.append("slug", teamSlug); 113 | 114 | const response = await fetch(url.toString(), { 115 | headers: { 116 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 117 | }, 118 | }); 119 | 120 | const data = await handleResponse(response); 121 | return { 122 | content: [{ type: "text", text: `Environment details:\n${JSON.stringify(data, null, 2)}` }], 123 | }; 124 | } 125 | ); 126 | 127 | // List Environments 128 | server.tool( 129 | "list_environments", 130 | "List custom environments for a project", 131 | { 132 | idOrName: z.string().describe("Project ID or name"), 133 | gitBranch: z.string().optional().describe("Filter by git branch"), 134 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 135 | teamSlug: z.string().optional().describe("The Team slug to perform the request on behalf of") 136 | }, 137 | async ({ idOrName, gitBranch, teamId, teamSlug }) => { 138 | const url = new URL(`${BASE_URL}/v9/projects/${idOrName}/custom-environments`); 139 | if (gitBranch) url.searchParams.append("gitBranch", gitBranch); 140 | if (teamId) url.searchParams.append("teamId", teamId); 141 | if (teamSlug) url.searchParams.append("slug", teamSlug); 142 | 143 | const response = await fetch(url.toString(), { 144 | headers: { 145 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 146 | }, 147 | }); 148 | 149 | const data = await handleResponse(response); 150 | return { 151 | content: [{ type: "text", text: `Environments list:\n${JSON.stringify(data, null, 2)}` }], 152 | }; 153 | } 154 | ); 155 | 156 | // Update Environment 157 | server.tool( 158 | "update_environment", 159 | "Update a custom environment", 160 | { 161 | idOrName: z.string().describe("Project ID or name"), 162 | environmentSlugOrId: z.string().describe("Environment slug or ID"), 163 | slug: z.string().max(32).optional().describe("New environment slug"), 164 | description: z.string().max(256).optional().describe("New environment description"), 165 | branchMatcher: z.object({ 166 | type: z.enum(["equals"]), 167 | pattern: z.string() 168 | }).nullable().optional().describe("New branch matching configuration"), 169 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 170 | teamSlug: z.string().optional().describe("The Team slug to perform the request on behalf of") 171 | }, 172 | async ({ idOrName, environmentSlugOrId, slug, description, branchMatcher, teamId, teamSlug }) => { 173 | const url = new URL(`${BASE_URL}/v9/projects/${idOrName}/custom-environments/${environmentSlugOrId}`); 174 | if (teamId) url.searchParams.append("teamId", teamId); 175 | if (teamSlug) url.searchParams.append("slug", teamSlug); 176 | 177 | const response = await fetch(url.toString(), { 178 | method: "PATCH", 179 | headers: { 180 | "Content-Type": "application/json", 181 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 182 | }, 183 | body: JSON.stringify({ slug, description, branchMatcher }), 184 | }); 185 | 186 | const data = await handleResponse(response); 187 | return { 188 | content: [{ type: "text", text: `Environment updated:\n${JSON.stringify(data, null, 2)}` }], 189 | }; 190 | } 191 | ); 192 | } 193 | -------------------------------------------------------------------------------- /src/components/artifacts.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerArtifactTools(server: McpServer) { 7 | // Check if artifact exists 8 | server.tool( 9 | "check_artifact", 10 | "Check that a cache artifact with the given hash exists", 11 | { 12 | hash: z.string().describe("The artifact hash"), 13 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 14 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 15 | }, 16 | async ({ hash, teamId, slug }) => { 17 | const url = new URL(`${BASE_URL}/v8/artifacts/${hash}`); 18 | if (teamId) url.searchParams.append("teamId", teamId); 19 | if (slug) url.searchParams.append("slug", slug); 20 | 21 | const response = await fetch(url.toString(), { 22 | method: "HEAD", 23 | headers: { 24 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 25 | }, 26 | }); 27 | 28 | await handleResponse(response); 29 | return { 30 | content: [{ type: "text", text: "Artifact exists" }], 31 | }; 32 | } 33 | ); 34 | 35 | // Download artifact 36 | server.tool( 37 | "download_artifact", 38 | "Downloads a cache artifact identified by its hash", 39 | { 40 | hash: z.string().describe("The artifact hash"), 41 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 42 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of"), 43 | ci: z.string().optional().describe("The CI environment where this artifact is downloaded"), 44 | interactive: z.boolean().optional().describe("Whether the client is an interactive shell") 45 | }, 46 | async ({ hash, teamId, slug, ci, interactive }) => { 47 | const url = new URL(`${BASE_URL}/v8/artifacts/${hash}`); 48 | if (teamId) url.searchParams.append("teamId", teamId); 49 | if (slug) url.searchParams.append("slug", slug); 50 | 51 | const headers: Record = { 52 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 53 | }; 54 | 55 | if (ci) headers["x-artifact-client-ci"] = ci; 56 | if (interactive !== undefined) headers["x-artifact-client-interactive"] = interactive ? "1" : "0"; 57 | 58 | const response = await fetch(url.toString(), { headers }); 59 | const data = await response.blob(); 60 | 61 | return { 62 | content: [{ type: "text", text: `Artifact downloaded: ${data.size} bytes` }], 63 | }; 64 | } 65 | ); 66 | 67 | // Get Remote Caching Status 68 | server.tool( 69 | "get_artifact_status", 70 | "Check the status of Remote Caching for this principal", 71 | { 72 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 73 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 74 | }, 75 | async ({ teamId, slug }) => { 76 | const url = new URL(`${BASE_URL}/v8/artifacts/status`); 77 | if (teamId) url.searchParams.append("teamId", teamId); 78 | if (slug) url.searchParams.append("slug", slug); 79 | 80 | const response = await fetch(url.toString(), { 81 | headers: { 82 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 83 | }, 84 | }); 85 | 86 | const data = await handleResponse(response); 87 | return { 88 | content: [{ type: "text", text: `Remote Caching status:\n${JSON.stringify(data, null, 2)}` }], 89 | }; 90 | } 91 | ); 92 | 93 | // Query Artifact Info 94 | server.tool( 95 | "query_artifacts", 96 | "Query information about an array of artifacts", 97 | { 98 | hashes: z.array(z.string()).describe("Array of artifact hashes"), 99 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 100 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 101 | }, 102 | async ({ hashes, teamId, slug }) => { 103 | const url = new URL(`${BASE_URL}/v8/artifacts`); 104 | if (teamId) url.searchParams.append("teamId", teamId); 105 | if (slug) url.searchParams.append("slug", slug); 106 | 107 | const response = await fetch(url.toString(), { 108 | method: "POST", 109 | headers: { 110 | "Content-Type": "application/json", 111 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 112 | }, 113 | body: JSON.stringify({ hashes }), 114 | }); 115 | 116 | const data = await handleResponse(response); 117 | return { 118 | content: [{ type: "text", text: `Artifacts information:\n${JSON.stringify(data, null, 2)}` }], 119 | }; 120 | } 121 | ); 122 | 123 | // Record Cache Usage Events 124 | server.tool( 125 | "record_artifact_events", 126 | "Records artifacts cache usage events", 127 | { 128 | events: z.array(z.object({ 129 | sessionId: z.string(), 130 | source: z.enum(["LOCAL", "REMOTE"]), 131 | event: z.enum(["HIT", "MISS"]), 132 | hash: z.string(), 133 | duration: z.number().optional() 134 | })).describe("Array of cache usage events"), 135 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 136 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of"), 137 | ci: z.string().optional().describe("The CI environment where events occurred"), 138 | interactive: z.boolean().optional().describe("Whether the client is an interactive shell") 139 | }, 140 | async ({ events, teamId, slug, ci, interactive }) => { 141 | const url = new URL(`${BASE_URL}/v8/artifacts/events`); 142 | if (teamId) url.searchParams.append("teamId", teamId); 143 | if (slug) url.searchParams.append("slug", slug); 144 | 145 | const headers: Record = { 146 | "Content-Type": "application/json", 147 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 148 | }; 149 | 150 | if (ci) headers["x-artifact-client-ci"] = ci; 151 | if (interactive !== undefined) headers["x-artifact-client-interactive"] = interactive ? "1" : "0"; 152 | 153 | const response = await fetch(url.toString(), { 154 | method: "POST", 155 | headers, 156 | body: JSON.stringify(events), 157 | }); 158 | 159 | await handleResponse(response); 160 | return { 161 | content: [{ type: "text", text: "Cache events recorded successfully" }], 162 | }; 163 | } 164 | ); 165 | 166 | // Upload Artifact 167 | server.tool( 168 | "upload_artifact", 169 | "Uploads a cache artifact identified by its hash", 170 | { 171 | hash: z.string().describe("The artifact hash"), 172 | content: z.string().describe("Base64 encoded content of the artifact"), 173 | contentLength: z.number().describe("The artifact size in bytes"), 174 | duration: z.number().optional().describe("Time taken to generate the artifact in milliseconds"), 175 | ci: z.string().optional().describe("The CI environment where this artifact was generated"), 176 | interactive: z.boolean().optional().describe("Whether the client is an interactive shell"), 177 | tag: z.string().optional().describe("Base64 encoded tag for this artifact"), 178 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 179 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 180 | }, 181 | async ({ hash, content, contentLength, duration, ci, interactive, tag, teamId, slug }) => { 182 | const url = new URL(`${BASE_URL}/v8/artifacts/${hash}`); 183 | if (teamId) url.searchParams.append("teamId", teamId); 184 | if (slug) url.searchParams.append("slug", slug); 185 | 186 | const headers: Record = { 187 | "Content-Type": "application/octet-stream", 188 | "Content-Length": contentLength.toString(), 189 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 190 | }; 191 | 192 | if (duration) headers["x-artifact-duration"] = duration.toString(); 193 | if (ci) headers["x-artifact-client-ci"] = ci; 194 | if (interactive !== undefined) headers["x-artifact-client-interactive"] = interactive ? "1" : "0"; 195 | if (tag) headers["x-artifact-tag"] = tag; 196 | 197 | const response = await fetch(url.toString(), { 198 | method: "PUT", 199 | headers, 200 | body: Buffer.from(content, "base64"), 201 | }); 202 | 203 | const data = await handleResponse(response); 204 | return { 205 | content: [{ type: "text", text: `Artifact uploaded:\n${JSON.stringify(data, null, 2)}` }], 206 | }; 207 | } 208 | ); 209 | } -------------------------------------------------------------------------------- /src/components/logDrains.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerLogDrainTools(server: McpServer) { 7 | // Create configurable log drain 8 | server.tool( 9 | "logdrain_create", 10 | "Creates a configurable log drain", 11 | { 12 | deliveryFormat: z.enum(["json", "ndjson"]).describe("The delivery log format"), 13 | url: z.string().url().regex(/^(http|https)?:\/\//).describe("The log drain url"), 14 | headers: z.record(z.string()).optional().describe("Headers to be sent together with the request"), 15 | projectIds: z.array(z.string()).min(1).max(50).describe("Project IDs to watch"), 16 | sources: z.array(z.enum(["static", "lambda", "build", "edge", "external", "firewall"])).min(1).describe("Sources to watch"), 17 | environments: z.array(z.enum(["preview", "production"])).min(1).describe("Environments to watch"), 18 | secret: z.string().optional().describe("Custom secret of log drain"), 19 | samplingRate: z.number().min(0.01).max(1).optional().describe("The sampling rate for this log drain"), 20 | name: z.string().optional().describe("The custom name of this log drain"), 21 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 22 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 23 | }, 24 | async ({ deliveryFormat, url, headers, projectIds, sources, environments, secret, samplingRate, name, teamId, slug }) => { 25 | const apiUrl = new URL(`${BASE_URL}/v1/log-drains`); 26 | if (teamId) apiUrl.searchParams.append("teamId", teamId); 27 | if (slug) apiUrl.searchParams.append("slug", slug); 28 | 29 | const response = await fetch(apiUrl.toString(), { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` 34 | }, 35 | body: JSON.stringify({ 36 | deliveryFormat, 37 | url, 38 | headers, 39 | projectIds, 40 | sources, 41 | environments, 42 | secret, 43 | samplingRate, 44 | name 45 | }) 46 | }); 47 | 48 | const data = await handleResponse(response); 49 | return { 50 | content: [{ type: "text", text: `Log drain created:\n${JSON.stringify(data, null, 2)}` }] 51 | }; 52 | } 53 | ); 54 | 55 | // Create integration log drain 56 | server.tool( 57 | "logdrain_create_integration", 58 | "Creates an integration log drain", 59 | { 60 | name: z.string().max(100).regex(/^[A-z0-9_ -]+$/).describe("The name of the log drain"), 61 | projectIds: z.array(z.string()).min(1).max(50).optional().describe("Project IDs to watch"), 62 | secret: z.string().max(100).regex(/^[A-z0-9_ -]+$/).optional().describe("Secret to sign log drain notifications"), 63 | deliveryFormat: z.enum(["json", "ndjson", "syslog"]).describe("The delivery log format"), 64 | url: z.string().url().regex(/^(https?|syslog\+tls|syslog):\/\//).describe("The url where you will receive logs"), 65 | sources: z.array(z.enum(["static", "lambda", "build", "edge", "external", "firewall"])).optional().describe("Sources to watch"), 66 | headers: z.record(z.string()).optional().describe("Headers to be sent together with the request"), 67 | environments: z.array(z.enum(["preview", "production"])).optional().describe("Environments to watch"), 68 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 69 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 70 | }, 71 | async ({ name, projectIds, secret, deliveryFormat, url, sources, headers, environments, teamId, slug }) => { 72 | const apiUrl = new URL(`${BASE_URL}/v2/integrations/log-drains`); 73 | if (teamId) apiUrl.searchParams.append("teamId", teamId); 74 | if (slug) apiUrl.searchParams.append("slug", slug); 75 | 76 | const response = await fetch(apiUrl.toString(), { 77 | method: "POST", 78 | headers: { 79 | "Content-Type": "application/json", 80 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` 81 | }, 82 | body: JSON.stringify({ 83 | name, 84 | projectIds, 85 | secret, 86 | deliveryFormat, 87 | url, 88 | sources, 89 | headers, 90 | environments 91 | }) 92 | }); 93 | 94 | const data = await handleResponse(response); 95 | return { 96 | content: [{ type: "text", text: `Integration log drain created:\n${JSON.stringify(data, null, 2)}` }] 97 | }; 98 | } 99 | ); 100 | 101 | // Delete log drain 102 | server.tool( 103 | "logdrain_delete", 104 | "Deletes a configurable log drain", 105 | { 106 | id: z.string().describe("The log drain ID to delete"), 107 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 108 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 109 | }, 110 | async ({ id, teamId, slug }) => { 111 | const url = new URL(`${BASE_URL}/v1/log-drains/${id}`); 112 | if (teamId) url.searchParams.append("teamId", teamId); 113 | if (slug) url.searchParams.append("slug", slug); 114 | 115 | const response = await fetch(url.toString(), { 116 | method: "DELETE", 117 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 118 | }); 119 | 120 | await handleResponse(response); 121 | return { 122 | content: [{ type: "text", text: "Log drain deleted successfully" }] 123 | }; 124 | } 125 | ); 126 | 127 | // Delete integration log drain 128 | server.tool( 129 | "logdrain_delete_integration", 130 | "Deletes an integration log drain", 131 | { 132 | id: z.string().describe("ID of the log drain to be deleted"), 133 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 134 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 135 | }, 136 | async ({ id, teamId, slug }) => { 137 | const url = new URL(`${BASE_URL}/v1/integrations/log-drains/${id}`); 138 | if (teamId) url.searchParams.append("teamId", teamId); 139 | if (slug) url.searchParams.append("slug", slug); 140 | 141 | const response = await fetch(url.toString(), { 142 | method: "DELETE", 143 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 144 | }); 145 | 146 | await handleResponse(response); 147 | return { 148 | content: [{ type: "text", text: "Integration log drain deleted successfully" }] 149 | }; 150 | } 151 | ); 152 | 153 | // Get log drain 154 | server.tool( 155 | "logdrain_get", 156 | "Retrieves a configurable log drain", 157 | { 158 | id: z.string().describe("The log drain ID"), 159 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 160 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 161 | }, 162 | async ({ id, teamId, slug }) => { 163 | const url = new URL(`${BASE_URL}/v1/log-drains/${id}`); 164 | if (teamId) url.searchParams.append("teamId", teamId); 165 | if (slug) url.searchParams.append("slug", slug); 166 | 167 | const response = await fetch(url.toString(), { 168 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 169 | }); 170 | 171 | const data = await handleResponse(response); 172 | return { 173 | content: [{ type: "text", text: `Log drain details:\n${JSON.stringify(data, null, 2)}` }] 174 | }; 175 | } 176 | ); 177 | 178 | // List log drains 179 | server.tool( 180 | "logdrain_list", 181 | "Retrieves a list of all log drains", 182 | { 183 | projectId: z.string().regex(/^[a-zA-z0-9_]+$/).optional().describe("Filter by project ID"), 184 | projectIdOrName: z.string().optional().describe("Filter by project ID or name"), 185 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 186 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 187 | }, 188 | async ({ projectId, projectIdOrName, teamId, slug }) => { 189 | const url = new URL(`${BASE_URL}/v1/log-drains`); 190 | if (projectId) url.searchParams.append("projectId", projectId); 191 | if (projectIdOrName) url.searchParams.append("projectIdOrName", projectIdOrName); 192 | if (teamId) url.searchParams.append("teamId", teamId); 193 | if (slug) url.searchParams.append("slug", slug); 194 | 195 | const response = await fetch(url.toString(), { 196 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 197 | }); 198 | 199 | const data = await handleResponse(response); 200 | return { 201 | content: [{ type: "text", text: `Log drains list:\n${JSON.stringify(data, null, 2)}` }] 202 | }; 203 | } 204 | ); 205 | 206 | // List integration log drains 207 | server.tool( 208 | "logdrain_list_integration", 209 | "Retrieves a list of integration log drains", 210 | { 211 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 212 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 213 | }, 214 | async ({ teamId, slug }) => { 215 | const url = new URL(`${BASE_URL}/v2/integrations/log-drains`); 216 | if (teamId) url.searchParams.append("teamId", teamId); 217 | if (slug) url.searchParams.append("slug", slug); 218 | 219 | const response = await fetch(url.toString(), { 220 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 221 | }); 222 | 223 | const data = await handleResponse(response); 224 | return { 225 | content: [{ type: "text", text: `Integration log drains list:\n${JSON.stringify(data, null, 2)}` }] 226 | }; 227 | } 228 | ); 229 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vercel MCP Server 🚀 2 | 3 | [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 4 | [![Vercel](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white)](https://vercel.com/) 5 | [![Node.js](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) 6 | [![MCP](https://img.shields.io/badge/MCP-Cursor-blue?style=for-the-badge)](https://cursor.sh/) 7 | [![Windsurf](https://img.shields.io/badge/Windsurf-Cascade-purple?style=for-the-badge)](https://www.codeium.com/cascade) 8 | 9 | > 🔥 A powerful Model Context Protocol (MCP) server that provides full administrative control over your Vercel deployments through both Cursor's Composer and Codeium's Cascade. This tool enables seamless project management with comprehensive features for deployments, domains, environment variables, and more. 10 | 11 |
12 | Vercel 13 |
14 | 15 | ## 📚 Table of Contents 16 | - [Prerequisites](#-prerequisites) 17 | - [Quick Start](#-quick-start) 18 | - [Integrations](#-integrations) 19 | - [Features](#-features) 20 | - [Usage](#-usage) 21 | - [Security Notes](#-security-notes) 22 | - [Troubleshooting](#-troubleshooting) 23 | - [Contributing](#-contributing) 24 | - [License](#-license) 25 | 26 | ## 🔧 Prerequisites 27 | 28 | - Node.js >= 16.x 29 | - npm >= 8.x 30 | - A Vercel account with: 31 | - Access Token 32 | - Team ID (optional) 33 | - Project ID (optional) 34 | - Cursor IDE or Codeium's Cascade (for paying users) 35 | 36 | ## 🚀 Quick Start 37 | 38 | ### 📥 Installation 39 | 40 | ```bash 41 | # Clone the repository 42 | git clone https://github.com/Quegenx/vercel-mcp-server.git 43 | cd vercel-mcp-server 44 | 45 | # Install dependencies 46 | npm install 47 | 48 | # Build the project 49 | npm run build 50 | ``` 51 | 52 | ### ⚙️ Configuration 53 | 54 | 1. Install dependencies and build the project: 55 | ```bash 56 | npm install 57 | npm run build 58 | ``` 59 | 60 | 2. Set up your Vercel access token: 61 | - Go to https://vercel.com/account/tokens to generate your access token 62 | - Update the token in both of these files: 63 | 64 | In `src/config/constants.ts`: 65 | ```typescript 66 | export const DEFAULT_ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"; // Replace with your actual token 67 | ``` 68 | 69 | In `src/index.ts`: 70 | ```typescript 71 | export const DEFAULT_ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"; // Replace with your actual token 72 | ``` 73 | 74 | 3. In Cursor's MCP settings, add the server with this command: 75 | 76 | For macOS: 77 | ```bash 78 | # Default installation 79 | /usr/local/bin/node /path/to/vercel-mcp/dist/index.js 80 | 81 | # Homebrew installation 82 | /opt/homebrew/bin/node /path/to/vercel-mcp/dist/index.js 83 | 84 | # NVM installation 85 | ~/.nvm/versions/node/v18.x.x/bin/node /path/to/vercel-mcp/dist/index.js 86 | ``` 87 | 88 | For Windows: 89 | ```bash 90 | # Default installation 91 | C:\Program Files\nodejs\node.exe C:\path\to\vercel-mcp\dist\index.js 92 | 93 | # NVM for Windows 94 | C:\nvm4w\nodejs\node.exe C:\path\to\vercel-mcp\dist\index.js 95 | 96 | # Scoop installation 97 | C:\Users\username\scoop\apps\nodejs\current\node.exe C:\path\to\vercel-mcp\dist\index.js 98 | ``` 99 | 100 | For Linux: 101 | ```bash 102 | # Default installation 103 | /usr/bin/node /path/to/vercel-mcp/dist/index.js 104 | 105 | # NVM installation 106 | ~/.nvm/versions/node/v18.x.x/bin/node /path/to/vercel-mcp/dist/index.js 107 | ``` 108 | 109 | Replace `/path/to/vercel-mcp` or `C:\path\to\vercel-mcp` with your actual installation path. 110 | 111 | To find your Node.js path: 112 | ```bash 113 | # macOS/Linux 114 | which node 115 | 116 | # Windows 117 | where node 118 | ``` 119 | 120 | Note: Keep your Vercel access token secure and never commit it to version control. 121 | 122 | ## 🎯 Features 123 | 124 | ### 🎯 Available Tools 125 | 126 | #### Team Management 127 | - Teams: `create_team`, `delete_team`, `get_team`, `list_teams`, `update_team` 128 | - Team Members: `list_team_members`, `invite_team_member`, `remove_team_member`, `update_team_member` 129 | 130 | #### Project Management 131 | - Projects: `list_projects`, `create_project`, `delete_project`, `update_project`, `pause_project` 132 | - Project Members: `add_project_member`, `list_project_members`, `remove_project_member` 133 | - Project Transfer: `request_project_transfer`, `accept_project_transfer` 134 | 135 | #### Deployment Management 136 | - Deployments: `create_deployment`, `cancel_deployment`, `get_deployment`, `delete_deployment`, `list_deployment` 137 | - Deployment Events: `get_deployment_events`, `update_deployment_integration` 138 | - Deployment Files: `list_deployment_files`, `upload_deployment_files`, `get_deployment_file` 139 | - Promotion: `promote_deployment`, `get_promotion_aliases` 140 | 141 | #### Domain & DNS Management 142 | - Domains: `add_domain`, `remove_domain`, `get_domain`, `list_domains`, `get_project_domain` 143 | - Domain Operations: `domain_check`, `domain_price`, `domain_config`, `domain_registry`, `domain_get`, `domain_list`, `domain_buy`, `domain_register`, `domain_remove`, `domain_update` 144 | - DNS: `create_dns_record`, `delete_dns_record`, `list_dns_records`, `update_dns_record` 145 | - Certificates: `get_cert`, `issue_cert`, `remove_cert`, `upload_cert` 146 | 147 | #### Environment & Configuration 148 | - Environment Variables: `add_env`, `update_env`, `delete_env`, `get_env`, `list_env` 149 | - Edge Config: `create_edge_config`, `update_edge_config`, `delete_edge_config`, `get_edge_config`, `list_edge_configs` 150 | - Edge Config Items: `list_edge_config_items`, `get_edge_config_item`, `update_edge_config_items` 151 | - Edge Config Schema: `get_edge_config_schema`, `update_edge_config_schema`, `delete_edge_config_schema` 152 | - Edge Config Tokens: `create_edge_config_token`, `get_edge_config_token`, `list_edge_config_tokens`, `delete_edge_config_tokens` 153 | - Edge Config Backups: `list_edge_config_backups`, `get_edge_config_backup` 154 | 155 | #### Access Control & Security 156 | - Access Groups: `create_access_group`, `delete_access_group`, `update_access_group`, `get_access_group`, `list_access_groups` 157 | - Access Group Projects: `create_access_group_project`, `delete_access_group_project`, `get_access_group_project`, `list_access_group_projects` 158 | - Access Group Members: `list_access_group_members` 159 | - Authentication: `create_auth_token`, `delete_auth_token`, `get_auth_token`, `list_auth_tokens`, `sso_token_exchange` 160 | - Firewall: `create_firewall_bypass`, `delete_firewall_bypass`, `get_firewall_bypass`, `get_attack_status`, `update_attack_mode`, `get_firewall_config`, `update_firewall_config`, `put_firewall_config` 161 | 162 | #### Monitoring & Logging 163 | - Log Drains: `logdrain_create`, `logdrain_createIntegration`, `logdrain_delete`, `logdrain_deleteIntegration`, `logdrain_get`, `logdrain_list`, `logdrain_listIntegration` 164 | - Webhooks: `create_webhook`, `delete_webhook`, `list_webhooks`, `get_webhook` 165 | - Analytics: `send_web_vitals` 166 | 167 | #### User Management 168 | - Users: `delete_user`, `get_user`, `list_user_events` 169 | 170 | #### Marketplace & Integration 171 | - Marketplace: `create_marketplace_event`, `get_marketplace_account`, `get_marketplace_invoice`, `get_marketplace_member`, `import_marketplace_resource`, `submit_marketplace_billing`, `submit_marketplace_invoice`, `update_marketplace_secrets`, `marketplace_sso_token_exchange`, `submit_marketplace_balance`, `marketplace_invoice_action` 172 | - Integrations: `int_delete`, `int_list`, `int_gitns`, `int_searchRepo`, `int_get`, `int_updateAction` 173 | 174 | #### Environments & Secrets 175 | - Environments: `create_environment`, `delete_environment`, `get_environment`, `list_environments`, `update_environment` 176 | - Secrets: `create_secret`, `update_secret_name`, `delete_secret`, `get_secret`, `list_secrets` 177 | 178 | #### Artifacts & Aliases 179 | - Artifacts: `check_artifact`, `download_artifact`, `get_artifact_status`, `query_artifacts`, `record_artifact_events`, `upload_artifact` 180 | - Aliases: `assign_alias`, `delete_alias`, `get_alias`, `list_aliases`, `list_deployment_aliases` 181 | 182 | ## 💡 Usage 183 | 184 | Once configured, the MCP server provides all Vercel management tools through Cursor's Composer. Simply describe what you want to do with your Vercel projects, and the AI will use the appropriate commands. 185 | 186 | Examples: 187 | - 📋 "List all my projects" 188 | - 🚀 "Create a new Next.js project" 189 | - 🌐 "Add a custom domain to my project" 190 | - 🔑 "Set up environment variables" 191 | 192 | ## 🔒 Security Notes 193 | 194 | - 🔐 Keep your Vercel access token secure 195 | - ⚠️ Never commit sensitive credentials to version control 196 | - 👮 Use appropriate access controls and permissions 197 | - 🛡️ Follow Vercel's security best practices 198 | 199 | ## 🛠️ Troubleshooting 200 | 201 | ### Common Issues 202 | 203 | 1. **Node.js Path Issues** 204 | - Ensure you're using the correct Node.js path 205 | - On Mac/Linux: Use `which node` to find the correct path 206 | - On Windows: Use `where node` to find the correct path 207 | 208 | 2. **Access Token Issues** 209 | - Verify your Vercel access token is valid 210 | - Check if the token has the required permissions 211 | - Ensure the token hasn't expired 212 | 213 | 3. **MCP Not Detecting Tools** 214 | - Click the refresh button in Cursor's MCP settings 215 | - Ensure the server is running (no error messages) 216 | - Verify your Vercel credentials are valid 217 | 218 | ### Debug Mode 219 | 220 | Add `DEBUG=true` before your command to see detailed logs: 221 | 222 | ```bash 223 | # macOS/Linux 224 | DEBUG=true /usr/local/bin/node /path/to/vercel-mcp/dist/index.js 225 | 226 | # Windows 227 | set DEBUG=true && "C:\Program Files\nodejs\node.exe" "C:\path\to\vercel-mcp\dist\index.js" 228 | ``` 229 | 230 | If you're still experiencing issues, please open an issue with: 231 | - Your operating system 232 | - Node.js version (`node --version`) 233 | - Full error message 234 | - Steps to reproduce 235 | 236 | ## 🤝 Contributing 237 | 238 | Contributions are welcome! Please feel free to submit a Pull Request. 239 | 240 | ## 📄 License 241 | 242 | --- 243 | 244 |
245 |

Built with ❤️ for the Cursor community

246 |

247 | Cursor • 248 | Vercel • 249 | GitHub 250 |

251 |
-------------------------------------------------------------------------------- /src/components/teams.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerTeamTools(server: McpServer) { 7 | // Create a Team 8 | server.tool( 9 | "create_team", 10 | "Create a new Team under your account", 11 | { 12 | slug: z.string().max(48).describe("The desired slug for the Team"), 13 | name: z.string().max(256).optional().describe("The desired name for the Team"), 14 | attribution: z.object({ 15 | sessionReferrer: z.string().optional(), 16 | landingPage: z.string().optional(), 17 | pageBeforeConversionPage: z.string().optional(), 18 | utm: z.object({ 19 | utmSource: z.string().optional(), 20 | utmMedium: z.string().optional(), 21 | utmCampaign: z.string().optional(), 22 | utmTerm: z.string().optional() 23 | }).optional() 24 | }).optional().describe("Attribution information") 25 | }, 26 | async ({ slug, name, attribution }) => { 27 | const response = await fetch(`${BASE_URL}/v1/teams`, { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 32 | }, 33 | body: JSON.stringify({ slug, name, attribution }), 34 | }); 35 | 36 | const data = await handleResponse(response); 37 | return { 38 | content: [ 39 | { type: "text", text: `Team created:\n${JSON.stringify(data, null, 2)}` }, 40 | ], 41 | }; 42 | } 43 | ); 44 | 45 | // Delete a Team 46 | server.tool( 47 | "delete_team", 48 | "Delete a team under your account", 49 | { 50 | teamId: z.string().describe("The Team identifier"), 51 | newDefaultTeamId: z.string().optional().describe("Id of the team to be set as the new default team"), 52 | reasons: z.array(z.object({ 53 | slug: z.string().describe("Reason identifier"), 54 | description: z.string().describe("Detailed description") 55 | })).optional().describe("Reasons for team deletion") 56 | }, 57 | async ({ teamId, newDefaultTeamId, reasons }) => { 58 | const url = new URL(`${BASE_URL}/v1/teams/${teamId}`); 59 | if (newDefaultTeamId) url.searchParams.append("newDefaultTeamId", newDefaultTeamId); 60 | 61 | const response = await fetch(url.toString(), { 62 | method: "DELETE", 63 | headers: { 64 | "Content-Type": "application/json", 65 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 66 | }, 67 | ...(reasons && { body: JSON.stringify({ reasons }) }) 68 | }); 69 | 70 | const data = await handleResponse(response); 71 | return { 72 | content: [ 73 | { type: "text", text: `Team deleted:\n${JSON.stringify(data, null, 2)}` }, 74 | ], 75 | }; 76 | } 77 | ); 78 | 79 | // Get Team Information 80 | server.tool( 81 | "get_team", 82 | "Get information for a specific team", 83 | { 84 | teamId: z.string().describe("The Team identifier") 85 | }, 86 | async ({ teamId }) => { 87 | const response = await fetch(`${BASE_URL}/v2/teams/${teamId}`, { 88 | headers: { 89 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 90 | }, 91 | }); 92 | 93 | const data = await handleResponse(response); 94 | return { 95 | content: [ 96 | { type: "text", text: `Team information:\n${JSON.stringify(data, null, 2)}` }, 97 | ], 98 | }; 99 | } 100 | ); 101 | 102 | // List Teams 103 | server.tool( 104 | "list_teams", 105 | "Get a list of all teams the authenticated user is a member of", 106 | { 107 | limit: z.number().optional().describe("Maximum number of teams to return"), 108 | since: z.number().optional().describe("Include teams created since timestamp"), 109 | until: z.number().optional().describe("Include teams created until timestamp") 110 | }, 111 | async ({ limit, since, until }) => { 112 | const url = new URL(`${BASE_URL}/v2/teams`); 113 | if (limit) url.searchParams.append("limit", limit.toString()); 114 | if (since) url.searchParams.append("since", since.toString()); 115 | if (until) url.searchParams.append("until", until.toString()); 116 | 117 | const response = await fetch(url.toString(), { 118 | headers: { 119 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 120 | }, 121 | }); 122 | 123 | const data = await handleResponse(response); 124 | return { 125 | content: [ 126 | { type: "text", text: `Teams:\n${JSON.stringify(data, null, 2)}` }, 127 | ], 128 | }; 129 | } 130 | ); 131 | 132 | // List Team Members 133 | server.tool( 134 | "list_team_members", 135 | "Get a list of team members", 136 | { 137 | teamId: z.string().describe("The Team identifier"), 138 | limit: z.number().min(1).optional().describe("Maximum number of members to return"), 139 | since: z.number().optional().describe("Include members added since timestamp"), 140 | until: z.number().optional().describe("Include members added until timestamp"), 141 | search: z.string().optional().describe("Search by name, username, or email"), 142 | role: z.enum(["OWNER", "MEMBER", "DEVELOPER", "VIEWER", "BILLING", "CONTRIBUTOR"]).optional().describe("Filter by role"), 143 | excludeProject: z.string().optional().describe("Exclude members from specific project"), 144 | eligibleMembersForProjectId: z.string().optional().describe("Include members eligible for project") 145 | }, 146 | async ({ teamId, limit, since, until, search, role, excludeProject, eligibleMembersForProjectId }) => { 147 | const url = new URL(`${BASE_URL}/v2/teams/${teamId}/members`); 148 | if (limit) url.searchParams.append("limit", limit.toString()); 149 | if (since) url.searchParams.append("since", since.toString()); 150 | if (until) url.searchParams.append("until", until.toString()); 151 | if (search) url.searchParams.append("search", search); 152 | if (role) url.searchParams.append("role", role); 153 | if (excludeProject) url.searchParams.append("excludeProject", excludeProject); 154 | if (eligibleMembersForProjectId) url.searchParams.append("eligibleMembersForProjectId", eligibleMembersForProjectId); 155 | 156 | const response = await fetch(url.toString(), { 157 | headers: { 158 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 159 | }, 160 | }); 161 | 162 | const data = await handleResponse(response); 163 | return { 164 | content: [ 165 | { type: "text", text: `Team members:\n${JSON.stringify(data, null, 2)}` }, 166 | ], 167 | }; 168 | } 169 | ); 170 | 171 | // Invite Team Member 172 | server.tool( 173 | "invite_team_member", 174 | "Invite a user to join a team", 175 | { 176 | teamId: z.string().describe("The Team identifier"), 177 | role: z.enum(["OWNER", "MEMBER", "DEVELOPER", "SECURITY", "BILLING", "VIEWER", "CONTRIBUTOR"]).describe("The role to assign"), 178 | email: z.string().email().optional().describe("The email address to invite"), 179 | uid: z.string().optional().describe("The user ID to invite"), 180 | projects: z.array(z.object({ 181 | projectId: z.string(), 182 | role: z.enum(["ADMIN", "MEMBER", "VIEWER"]) 183 | })).optional().describe("Project-specific roles") 184 | }, 185 | async ({ teamId, role, email, uid, projects }) => { 186 | const response = await fetch(`${BASE_URL}/v1/teams/${teamId}/members`, { 187 | method: "POST", 188 | headers: { 189 | "Content-Type": "application/json", 190 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 191 | }, 192 | body: JSON.stringify({ role, email, uid, projects }), 193 | }); 194 | 195 | const data = await handleResponse(response); 196 | return { 197 | content: [ 198 | { type: "text", text: `Team member invited:\n${JSON.stringify(data, null, 2)}` }, 199 | ], 200 | }; 201 | } 202 | ); 203 | 204 | // Remove Team Member 205 | server.tool( 206 | "remove_team_member", 207 | "Remove a member from a team", 208 | { 209 | teamId: z.string().describe("The Team identifier"), 210 | uid: z.string().describe("The user ID to remove"), 211 | newDefaultTeamId: z.string().optional().describe("New default team ID for Northstar user") 212 | }, 213 | async ({ teamId, uid, newDefaultTeamId }) => { 214 | const url = new URL(`${BASE_URL}/v1/teams/${teamId}/members/${uid}`); 215 | if (newDefaultTeamId) url.searchParams.append("newDefaultTeamId", newDefaultTeamId); 216 | 217 | const response = await fetch(url.toString(), { 218 | method: "DELETE", 219 | headers: { 220 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 221 | }, 222 | }); 223 | 224 | const data = await handleResponse(response); 225 | return { 226 | content: [ 227 | { type: "text", text: `Team member removed:\n${JSON.stringify(data, null, 2)}` }, 228 | ], 229 | }; 230 | } 231 | ); 232 | 233 | // Update Team Member 234 | server.tool( 235 | "update_team_member", 236 | "Update a team member's role or status", 237 | { 238 | teamId: z.string().describe("The Team identifier"), 239 | uid: z.string().describe("The user ID to update"), 240 | confirmed: z.literal(true).optional().describe("Accept user's request to join"), 241 | role: z.enum(["MEMBER", "VIEWER"]).optional().describe("New role for the member"), 242 | projects: z.array(z.object({ 243 | projectId: z.string(), 244 | role: z.enum(["ADMIN", "MEMBER", "VIEWER"]) 245 | })).optional().describe("Project-specific roles"), 246 | joinedFrom: z.object({ 247 | ssoUserId: z.string().nullable() 248 | }).optional().describe("SSO connection information") 249 | }, 250 | async ({ teamId, uid, confirmed, role, projects, joinedFrom }) => { 251 | const response = await fetch(`${BASE_URL}/v1/teams/${teamId}/members/${uid}`, { 252 | method: "PATCH", 253 | headers: { 254 | "Content-Type": "application/json", 255 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 256 | }, 257 | body: JSON.stringify({ confirmed, role, projects, joinedFrom }), 258 | }); 259 | 260 | const data = await handleResponse(response); 261 | return { 262 | content: [ 263 | { type: "text", text: `Team member updated:\n${JSON.stringify(data, null, 2)}` }, 264 | ], 265 | }; 266 | } 267 | ); 268 | 269 | // Update Team 270 | server.tool( 271 | "update_team", 272 | "Update team information", 273 | { 274 | teamId: z.string().describe("The Team identifier"), 275 | name: z.string().max(256).optional().describe("Team name"), 276 | slug: z.string().optional().describe("New team slug"), 277 | description: z.string().max(140).optional().describe("Team description"), 278 | avatar: z.string().regex(/^[a-f0-9]+$/).optional().describe("Hash of uploaded image"), 279 | previewDeploymentSuffix: z.string().nullable().optional().describe("Preview deployment suffix"), 280 | emailDomain: z.union([z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}$/), z.null()]).optional().describe("Team email domain"), 281 | regenerateInviteCode: z.boolean().optional().describe("Create new invite code"), 282 | saml: z.object({ 283 | enforced: z.boolean(), 284 | roles: z.record(z.enum(["OWNER"])) 285 | }).optional().describe("SAML configuration"), 286 | enablePreviewFeedback: z.enum(["on", "off", "default"]).optional().describe("Preview toolbar setting"), 287 | enableProductionFeedback: z.enum(["on", "off", "default"]).optional().describe("Production toolbar setting"), 288 | sensitiveEnvironmentVariablePolicy: z.enum(["on", "off", "default"]).optional().describe("Sensitive env var policy"), 289 | remoteCaching: z.object({ 290 | enabled: z.boolean() 291 | }).optional().describe("Remote caching settings"), 292 | hideIpAddresses: z.boolean().optional().describe("Hide IP addresses in monitoring"), 293 | hideIpAddressesInLogDrains: z.boolean().optional().describe("Hide IP addresses in log drains") 294 | }, 295 | async ({ teamId, ...updateFields }) => { 296 | const response = await fetch(`${BASE_URL}/v2/teams/${teamId}`, { 297 | method: "PATCH", 298 | headers: { 299 | "Content-Type": "application/json", 300 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 301 | }, 302 | body: JSON.stringify(updateFields), 303 | }); 304 | 305 | const data = await handleResponse(response); 306 | return { 307 | content: [ 308 | { type: "text", text: `Team updated:\n${JSON.stringify(data, null, 2)}` }, 309 | ], 310 | }; 311 | } 312 | ); 313 | } -------------------------------------------------------------------------------- /src/components/marketplace.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | // Common schemas 7 | const SecretSchema = z.object({ 8 | name: z.string(), 9 | value: z.string(), 10 | prefix: z.string().optional() 11 | }); 12 | 13 | const PeriodSchema = z.object({ 14 | start: z.string(), 15 | end: z.string() 16 | }); 17 | 18 | const BillingItemSchema = z.object({ 19 | billingPlanId: z.string(), 20 | resourceId: z.string(), 21 | start: z.string(), 22 | end: z.string(), 23 | name: z.string(), 24 | details: z.string().optional(), 25 | price: z.string(), 26 | quantity: z.number(), 27 | units: z.string().optional(), 28 | total: z.string() 29 | }); 30 | 31 | const DiscountSchema = z.object({ 32 | billingPlanId: z.string(), 33 | resourceId: z.string(), 34 | start: z.string(), 35 | end: z.string(), 36 | name: z.string(), 37 | details: z.string().optional(), 38 | amount: z.string() 39 | }); 40 | 41 | const UsageSchema = z.object({ 42 | resourceId: z.string(), 43 | name: z.string(), 44 | type: z.enum(["total"]), 45 | units: z.string(), 46 | dayValue: z.number(), 47 | periodValue: z.number(), 48 | planValue: z.number() 49 | }); 50 | 51 | const NotificationSchema = z.object({ 52 | level: z.enum(["info", "warning", "error"]), 53 | title: z.string(), 54 | message: z.string(), 55 | href: z.string().optional() 56 | }); 57 | 58 | const BalanceSchema = z.object({ 59 | resourceId: z.string(), 60 | credit: z.string(), 61 | nameLabel: z.string(), 62 | currencyValueInCents: z.number() 63 | }); 64 | 65 | export function registerMarketplaceTools(server: McpServer) { 66 | // Create Event 67 | server.tool( 68 | "create_marketplace_event", 69 | "Create a marketplace event", 70 | { 71 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 72 | event: z.object({ 73 | type: z.enum(["resource.updated", "installation.updated"]), 74 | billingPlanId: z.string().optional() 75 | }).describe("Event details") 76 | }, 77 | async ({ integrationConfigurationId, event }) => { 78 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/events`, { 79 | method: "POST", 80 | headers: { 81 | "Content-Type": "application/json", 82 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 83 | }, 84 | body: JSON.stringify({ event }), 85 | }); 86 | 87 | await handleResponse(response); 88 | return { 89 | content: [{ type: "text", text: "Event created successfully" }], 90 | }; 91 | } 92 | ); 93 | 94 | // Get Account Information 95 | server.tool( 96 | "get_marketplace_account", 97 | "Get marketplace account information", 98 | { 99 | integrationConfigurationId: z.string().describe("Integration configuration ID") 100 | }, 101 | async ({ integrationConfigurationId }) => { 102 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/account`, { 103 | headers: { 104 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 105 | }, 106 | }); 107 | 108 | const data = await handleResponse(response); 109 | return { 110 | content: [{ type: "text", text: `Account information:\n${JSON.stringify(data, null, 2)}` }], 111 | }; 112 | } 113 | ); 114 | 115 | // Get Invoice 116 | server.tool( 117 | "get_marketplace_invoice", 118 | "Get marketplace invoice details", 119 | { 120 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 121 | invoiceId: z.string().describe("Invoice ID") 122 | }, 123 | async ({ integrationConfigurationId, invoiceId }) => { 124 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/billing/invoices/${invoiceId}`, { 125 | headers: { 126 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 127 | }, 128 | }); 129 | 130 | const data = await handleResponse(response); 131 | return { 132 | content: [{ type: "text", text: `Invoice details:\n${JSON.stringify(data, null, 2)}` }], 133 | }; 134 | } 135 | ); 136 | 137 | // Get Member Information 138 | server.tool( 139 | "get_marketplace_member", 140 | "Get marketplace member information", 141 | { 142 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 143 | memberId: z.string().describe("Member ID") 144 | }, 145 | async ({ integrationConfigurationId, memberId }) => { 146 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/member/${memberId}`, { 147 | headers: { 148 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 149 | }, 150 | }); 151 | 152 | const data = await handleResponse(response); 153 | return { 154 | content: [{ type: "text", text: `Member information:\n${JSON.stringify(data, null, 2)}` }], 155 | }; 156 | } 157 | ); 158 | 159 | // Import Resource 160 | server.tool( 161 | "import_marketplace_resource", 162 | "Import a marketplace resource", 163 | { 164 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 165 | resourceId: z.string().describe("Resource ID"), 166 | productId: z.string().describe("Product ID"), 167 | name: z.string().describe("Resource name"), 168 | status: z.enum(["ready", "pending", "suspended", "resumed", "uninstalled", "error"]).describe("Resource status"), 169 | metadata: z.record(z.any()).optional().describe("Additional metadata"), 170 | billingPlan: z.object({ 171 | id: z.string(), 172 | type: z.enum(["prepayment"]), 173 | name: z.string(), 174 | paymentMethodRequired: z.boolean() 175 | }).optional(), 176 | notification: NotificationSchema.optional(), 177 | secrets: z.array(SecretSchema).optional() 178 | }, 179 | async ({ integrationConfigurationId, resourceId, ...body }) => { 180 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/resources/${resourceId}`, { 181 | method: "PUT", 182 | headers: { 183 | "Content-Type": "application/json", 184 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 185 | }, 186 | body: JSON.stringify(body), 187 | }); 188 | 189 | const data = await handleResponse(response); 190 | return { 191 | content: [{ type: "text", text: `Resource imported:\n${JSON.stringify(data, null, 2)}` }], 192 | }; 193 | } 194 | ); 195 | 196 | // Submit Billing Data 197 | server.tool( 198 | "submit_marketplace_billing", 199 | "Submit marketplace billing data", 200 | { 201 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 202 | timestamp: z.string().describe("Server timestamp"), 203 | eod: z.string().describe("End of day timestamp"), 204 | period: PeriodSchema.describe("Billing period"), 205 | billing: z.array(BillingItemSchema).describe("Billing items"), 206 | usage: z.array(UsageSchema).describe("Usage data") 207 | }, 208 | async ({ integrationConfigurationId, ...body }) => { 209 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/billing`, { 210 | method: "POST", 211 | headers: { 212 | "Content-Type": "application/json", 213 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 214 | }, 215 | body: JSON.stringify(body), 216 | }); 217 | 218 | await handleResponse(response); 219 | return { 220 | content: [{ type: "text", text: "Billing data submitted successfully" }], 221 | }; 222 | } 223 | ); 224 | 225 | // Submit Invoice 226 | server.tool( 227 | "submit_marketplace_invoice", 228 | "Submit a marketplace invoice", 229 | { 230 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 231 | externalId: z.string().optional().describe("External invoice ID"), 232 | invoiceDate: z.string().describe("Invoice date"), 233 | memo: z.string().optional().describe("Invoice memo"), 234 | period: PeriodSchema.describe("Invoice period"), 235 | items: z.array(BillingItemSchema).describe("Invoice items"), 236 | discounts: z.array(DiscountSchema).optional().describe("Discount items"), 237 | test: z.object({ 238 | validate: z.boolean().optional(), 239 | result: z.enum(["paid"]).optional() 240 | }).optional() 241 | }, 242 | async ({ integrationConfigurationId, ...body }) => { 243 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/billing/invoices`, { 244 | method: "POST", 245 | headers: { 246 | "Content-Type": "application/json", 247 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 248 | }, 249 | body: JSON.stringify(body), 250 | }); 251 | 252 | const data = await handleResponse(response); 253 | return { 254 | content: [{ type: "text", text: `Invoice submitted:\n${JSON.stringify(data, null, 2)}` }], 255 | }; 256 | } 257 | ); 258 | 259 | // Update Resource Secrets 260 | server.tool( 261 | "update_marketplace_secrets", 262 | "Update marketplace resource secrets", 263 | { 264 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 265 | resourceId: z.string().describe("Resource ID"), 266 | secrets: z.array(SecretSchema).describe("Resource secrets") 267 | }, 268 | async ({ integrationConfigurationId, resourceId, secrets }) => { 269 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/resources/${resourceId}/secrets`, { 270 | method: "PUT", 271 | headers: { 272 | "Content-Type": "application/json", 273 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 274 | }, 275 | body: JSON.stringify({ secrets }), 276 | }); 277 | 278 | await handleResponse(response); 279 | return { 280 | content: [{ type: "text", text: "Resource secrets updated successfully" }], 281 | }; 282 | } 283 | ); 284 | 285 | // SSO Token Exchange 286 | server.tool( 287 | "marketplace_sso_token_exchange", 288 | "Exchange OAuth code for OIDC token", 289 | { 290 | code: z.string().describe("OAuth code"), 291 | clientId: z.string().describe("Client ID"), 292 | clientSecret: z.string().describe("Client secret"), 293 | state: z.string().optional().describe("OAuth state"), 294 | redirectUri: z.string().optional().describe("Redirect URI") 295 | }, 296 | async (body) => { 297 | const response = await fetch(`${BASE_URL}/v1/integrations/sso/token`, { 298 | method: "POST", 299 | headers: { 300 | "Content-Type": "application/json", 301 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 302 | }, 303 | body: JSON.stringify(body), 304 | }); 305 | 306 | const data = await handleResponse(response); 307 | return { 308 | content: [{ type: "text", text: `Token exchange completed:\n${JSON.stringify(data, null, 2)}` }], 309 | }; 310 | } 311 | ); 312 | 313 | // Submit Prepayment Balances 314 | server.tool( 315 | "submit_marketplace_balance", 316 | "Submit prepayment balances", 317 | { 318 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 319 | timestamp: z.string().describe("Server timestamp"), 320 | balances: z.array(BalanceSchema).describe("Balance data") 321 | }, 322 | async ({ integrationConfigurationId, ...body }) => { 323 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/billing/balance`, { 324 | method: "POST", 325 | headers: { 326 | "Content-Type": "application/json", 327 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 328 | }, 329 | body: JSON.stringify(body), 330 | }); 331 | 332 | await handleResponse(response); 333 | return { 334 | content: [{ type: "text", text: "Balance data submitted successfully" }], 335 | }; 336 | } 337 | ); 338 | 339 | // Invoice Actions (Refund) 340 | server.tool( 341 | "marketplace_invoice_action", 342 | "Perform invoice actions like refund", 343 | { 344 | integrationConfigurationId: z.string().describe("Integration configuration ID"), 345 | invoiceId: z.string().describe("Invoice ID"), 346 | action: z.literal("refund").describe("Action type"), 347 | reason: z.string().describe("Refund reason"), 348 | total: z.string().regex(/^[0-9]+(\.[0-9]+)?$/).describe("Refund amount") 349 | }, 350 | async ({ integrationConfigurationId, invoiceId, ...body }) => { 351 | const response = await fetch(`${BASE_URL}/v1/installations/${integrationConfigurationId}/billing/invoices/${invoiceId}/actions`, { 352 | method: "POST", 353 | headers: { 354 | "Content-Type": "application/json", 355 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 356 | }, 357 | body: JSON.stringify(body), 358 | }); 359 | 360 | await handleResponse(response); 361 | return { 362 | content: [{ type: "text", text: "Invoice action completed successfully" }], 363 | }; 364 | } 365 | ); 366 | } -------------------------------------------------------------------------------- /src/components/security.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | // Common parameter schemas 7 | const teamParams = { 8 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 9 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 10 | }; 11 | 12 | export function registerSecurityTools(server: McpServer) { 13 | // Create System Bypass Rule 14 | server.tool( 15 | "create_firewall_bypass", 16 | "Create new system bypass rules", 17 | { 18 | projectId: z.string().describe("Project ID"), 19 | ...teamParams, 20 | domain: z.string().max(2544).regex(/([a-z]+[a-z.]+)$/).optional().describe("Domain"), 21 | projectScope: z.boolean().optional().describe("If the specified bypass will apply to all domains for a project"), 22 | sourceIp: z.string().optional().describe("Source IP"), 23 | allSources: z.boolean().optional().describe("All sources"), 24 | ttl: z.number().optional().describe("Time to live in milliseconds"), 25 | note: z.string().max(500).optional().describe("Note") 26 | }, 27 | async ({ projectId, teamId, slug, ...bypassData }) => { 28 | const url = new URL(`${BASE_URL}/v1/security/firewall/bypass`); 29 | url.searchParams.append("projectId", projectId); 30 | if (teamId) url.searchParams.append("teamId", teamId); 31 | if (slug) url.searchParams.append("slug", slug); 32 | 33 | const response = await fetch(url.toString(), { 34 | method: "POST", 35 | headers: { 36 | "Content-Type": "application/json", 37 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 38 | }, 39 | body: JSON.stringify(bypassData), 40 | }); 41 | 42 | const data = await handleResponse(response); 43 | return { 44 | content: [ 45 | { type: "text", text: `Bypass rule created:\n${JSON.stringify(data, null, 2)}` }, 46 | ], 47 | }; 48 | } 49 | ); 50 | 51 | // Delete System Bypass Rule 52 | server.tool( 53 | "delete_firewall_bypass", 54 | "Remove system bypass rules", 55 | { 56 | projectId: z.string().describe("Project ID"), 57 | ...teamParams, 58 | domain: z.string().max(2544).regex(/([a-z]+[a-z.]+)$/).optional().describe("Domain"), 59 | projectScope: z.boolean().optional().describe("Project scope"), 60 | sourceIp: z.string().optional().describe("Source IP"), 61 | allSources: z.boolean().optional().describe("All sources"), 62 | note: z.string().max(500).optional().describe("Note") 63 | }, 64 | async ({ projectId, teamId, slug, ...bypassData }) => { 65 | const url = new URL(`${BASE_URL}/v1/security/firewall/bypass`); 66 | url.searchParams.append("projectId", projectId); 67 | if (teamId) url.searchParams.append("teamId", teamId); 68 | if (slug) url.searchParams.append("slug", slug); 69 | 70 | const response = await fetch(url.toString(), { 71 | method: "DELETE", 72 | headers: { 73 | "Content-Type": "application/json", 74 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 75 | }, 76 | body: JSON.stringify(bypassData), 77 | }); 78 | 79 | const data = await handleResponse(response); 80 | return { 81 | content: [ 82 | { type: "text", text: `Bypass rule deleted:\n${JSON.stringify(data, null, 2)}` }, 83 | ], 84 | }; 85 | } 86 | ); 87 | 88 | // Get System Bypass Rules 89 | server.tool( 90 | "get_firewall_bypass", 91 | "Retrieve the system bypass rules", 92 | { 93 | projectId: z.string().describe("Project ID"), 94 | limit: z.number().max(128).optional().describe("Maximum number of rules to return"), 95 | sourceIp: z.string().max(49).optional().describe("Filter by source IP"), 96 | domain: z.string().max(2544).regex(/([a-z]+[a-z.]+)$/).optional().describe("Filter by domain"), 97 | projectScope: z.boolean().optional().describe("Filter by project scoped rules"), 98 | offset: z.string().max(2560).optional().describe("Used for pagination"), 99 | ...teamParams 100 | }, 101 | async ({ projectId, teamId, slug, ...queryParams }) => { 102 | const url = new URL(`${BASE_URL}/v1/security/firewall/bypass`); 103 | url.searchParams.append("projectId", projectId); 104 | if (teamId) url.searchParams.append("teamId", teamId); 105 | if (slug) url.searchParams.append("slug", slug); 106 | Object.entries(queryParams).forEach(([key, value]) => { 107 | if (value !== undefined) url.searchParams.append(key, value.toString()); 108 | }); 109 | 110 | const response = await fetch(url.toString(), { 111 | headers: { 112 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 113 | }, 114 | }); 115 | 116 | const data = await handleResponse(response); 117 | return { 118 | content: [ 119 | { type: "text", text: `Bypass rules:\n${JSON.stringify(data, null, 2)}` }, 120 | ], 121 | }; 122 | } 123 | ); 124 | 125 | // Get Attack Status 126 | server.tool( 127 | "get_attack_status", 128 | "Retrieve active attack data within the last 24h window", 129 | { 130 | projectId: z.string().describe("Project ID"), 131 | ...teamParams 132 | }, 133 | async ({ projectId, teamId, slug }) => { 134 | const url = new URL(`${BASE_URL}/v1/security/firewall/attack-status`); 135 | url.searchParams.append("projectId", projectId); 136 | if (teamId) url.searchParams.append("teamId", teamId); 137 | if (slug) url.searchParams.append("slug", slug); 138 | 139 | const response = await fetch(url.toString(), { 140 | headers: { 141 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 142 | }, 143 | }); 144 | 145 | const data = await handleResponse(response); 146 | return { 147 | content: [ 148 | { type: "text", text: `Attack status:\n${JSON.stringify(data, null, 2)}` }, 149 | ], 150 | }; 151 | } 152 | ); 153 | 154 | // Update Attack Challenge Mode 155 | server.tool( 156 | "update_attack_mode", 157 | "Update the Attack Challenge mode settings", 158 | { 159 | projectId: z.string().describe("Project ID"), 160 | attackModeEnabled: z.boolean().describe("Enable/disable attack mode"), 161 | attackModeActiveUntil: z.number().nullable().optional().describe("Timestamp until attack mode is active"), 162 | ...teamParams 163 | }, 164 | async ({ projectId, attackModeEnabled, attackModeActiveUntil, teamId, slug }) => { 165 | const url = new URL(`${BASE_URL}/v1/security/attack-mode`); 166 | if (teamId) url.searchParams.append("teamId", teamId); 167 | if (slug) url.searchParams.append("slug", slug); 168 | 169 | const response = await fetch(url.toString(), { 170 | method: "POST", 171 | headers: { 172 | "Content-Type": "application/json", 173 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 174 | }, 175 | body: JSON.stringify({ 176 | projectId, 177 | attackModeEnabled, 178 | attackModeActiveUntil 179 | }), 180 | }); 181 | 182 | const data = await handleResponse(response); 183 | return { 184 | content: [ 185 | { type: "text", text: `Attack mode updated:\n${JSON.stringify(data, null, 2)}` }, 186 | ], 187 | }; 188 | } 189 | ); 190 | 191 | // Get Firewall Config 192 | server.tool( 193 | "get_firewall_config", 194 | "Retrieve the firewall configuration", 195 | { 196 | projectId: z.string().describe("Project ID"), 197 | configVersion: z.string().optional().describe("Configuration version"), 198 | ...teamParams 199 | }, 200 | async ({ projectId, configVersion, teamId, slug }) => { 201 | const url = new URL(`${BASE_URL}/v1/security/firewall/config${configVersion ? `/${configVersion}` : ''}`); 202 | url.searchParams.append("projectId", projectId); 203 | if (teamId) url.searchParams.append("teamId", teamId); 204 | if (slug) url.searchParams.append("slug", slug); 205 | 206 | const response = await fetch(url.toString(), { 207 | headers: { 208 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 209 | }, 210 | }); 211 | 212 | const data = await handleResponse(response); 213 | return { 214 | content: [ 215 | { type: "text", text: `Firewall configuration:\n${JSON.stringify(data, null, 2)}` }, 216 | ], 217 | }; 218 | } 219 | ); 220 | 221 | // Update Firewall Config 222 | server.tool( 223 | "update_firewall_config", 224 | "Update the firewall configuration", 225 | { 226 | projectId: z.string().describe("Project ID"), 227 | ...teamParams, 228 | action: z.string().describe("Action to perform"), 229 | id: z.string().nullable().optional().describe("Rule ID"), 230 | value: z.any().describe("Value for the action") 231 | }, 232 | async ({ projectId, teamId, slug, ...updateData }) => { 233 | const url = new URL(`${BASE_URL}/v1/security/firewall/config`); 234 | url.searchParams.append("projectId", projectId); 235 | if (teamId) url.searchParams.append("teamId", teamId); 236 | if (slug) url.searchParams.append("slug", slug); 237 | 238 | const response = await fetch(url.toString(), { 239 | method: "PATCH", 240 | headers: { 241 | "Content-Type": "application/json", 242 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 243 | }, 244 | body: JSON.stringify(updateData), 245 | }); 246 | 247 | const data = await handleResponse(response); 248 | return { 249 | content: [ 250 | { type: "text", text: `Firewall configuration updated:\n${JSON.stringify(data, null, 2)}` }, 251 | ], 252 | }; 253 | } 254 | ); 255 | 256 | // Put Firewall Config 257 | server.tool( 258 | "put_firewall_config", 259 | "Set the complete firewall configuration", 260 | { 261 | projectId: z.string().describe("Project ID"), 262 | ...teamParams, 263 | firewallEnabled: z.boolean().describe("Enable/disable firewall"), 264 | managedRules: z.object({ 265 | owasp: z.object({ 266 | active: z.boolean() 267 | }).optional() 268 | }).optional().describe("Managed rules configuration"), 269 | crs: z.object({ 270 | sd: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 271 | ma: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 272 | lfi: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 273 | rfi: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 274 | rce: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 275 | php: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 276 | gen: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 277 | xss: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 278 | sqli: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 279 | sf: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional(), 280 | java: z.object({ active: z.boolean(), action: z.enum(["deny"]) }).optional() 281 | }).optional().describe("Custom rule set configuration"), 282 | rules: z.array(z.object({ 283 | id: z.string(), 284 | name: z.string(), 285 | description: z.string(), 286 | active: z.boolean(), 287 | conditionGroup: z.array(z.object({ 288 | conditions: z.array(z.object({ 289 | type: z.string(), 290 | op: z.string(), 291 | neg: z.boolean(), 292 | key: z.string(), 293 | value: z.string() 294 | })) 295 | })), 296 | action: z.object({ 297 | mitigate: z.object({ 298 | action: z.enum(["log", "deny"]), 299 | rateLimit: z.object({ 300 | algo: z.enum(["fixed_window"]), 301 | window: z.number(), 302 | limit: z.number(), 303 | keys: z.array(z.string()), 304 | action: z.enum(["log", "deny"]) 305 | }).optional(), 306 | redirect: z.object({ 307 | location: z.string(), 308 | permanent: z.boolean() 309 | }).optional(), 310 | actionDuration: z.any().nullable(), 311 | bypassSystem: z.any().nullable() 312 | }) 313 | }) 314 | })).optional().describe("Custom rules"), 315 | ips: z.array(z.object({ 316 | id: z.string(), 317 | hostname: z.string(), 318 | ip: z.string(), 319 | notes: z.string(), 320 | action: z.enum(["deny"]) 321 | })).optional().describe("IP rules") 322 | }, 323 | async ({ projectId, teamId, slug, ...config }) => { 324 | const url = new URL(`${BASE_URL}/v1/security/firewall/config`); 325 | url.searchParams.append("projectId", projectId); 326 | if (teamId) url.searchParams.append("teamId", teamId); 327 | if (slug) url.searchParams.append("slug", slug); 328 | 329 | const response = await fetch(url.toString(), { 330 | method: "PUT", 331 | headers: { 332 | "Content-Type": "application/json", 333 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 334 | }, 335 | body: JSON.stringify(config), 336 | }); 337 | 338 | const data = await handleResponse(response); 339 | return { 340 | content: [ 341 | { type: "text", text: `Firewall configuration set:\n${JSON.stringify(data, null, 2)}` }, 342 | ], 343 | }; 344 | } 345 | ); 346 | } -------------------------------------------------------------------------------- /src/components/domains.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | export function registerDomainTools(server: McpServer) { 7 | // Check domain availability 8 | server.tool( 9 | "domain_check", 10 | "Check if a domain name is available for purchase", 11 | { 12 | name: z.string().describe("The name of the domain to check"), 13 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 14 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 15 | }, 16 | async ({ name, teamId, slug }) => { 17 | const url = new URL(`${BASE_URL}/v4/domains/status`); 18 | url.searchParams.append("name", name); 19 | if (teamId) url.searchParams.append("teamId", teamId); 20 | if (slug) url.searchParams.append("slug", slug); 21 | 22 | const response = await fetch(url.toString(), { 23 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 24 | }); 25 | 26 | const data = await handleResponse(response); 27 | return { 28 | content: [{ type: "text", text: `Domain availability:\n${JSON.stringify(data, null, 2)}` }] 29 | }; 30 | } 31 | ); 32 | 33 | // Get domain price 34 | server.tool( 35 | "domain_price", 36 | "Check the price to purchase a domain", 37 | { 38 | name: z.string().describe("The name of the domain to check price for"), 39 | type: z.enum(["new", "renewal", "transfer", "redemption"]).optional().describe("Domain status type to check price for"), 40 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 41 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 42 | }, 43 | async ({ name, type, teamId, slug }) => { 44 | const url = new URL(`${BASE_URL}/v4/domains/price`); 45 | url.searchParams.append("name", name); 46 | if (type) url.searchParams.append("type", type); 47 | if (teamId) url.searchParams.append("teamId", teamId); 48 | if (slug) url.searchParams.append("slug", slug); 49 | 50 | const response = await fetch(url.toString(), { 51 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 52 | }); 53 | 54 | const data = await handleResponse(response); 55 | return { 56 | content: [{ type: "text", text: `Domain price:\n${JSON.stringify(data, null, 2)}` }] 57 | }; 58 | } 59 | ); 60 | 61 | // Get domain config 62 | server.tool( 63 | "domain_config", 64 | "Get a Domain's configuration", 65 | { 66 | domain: z.string().describe("The name of the domain"), 67 | strict: z.boolean().optional().describe("When true, only include nameservers assigned directly to the domain"), 68 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 69 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 70 | }, 71 | async ({ domain, strict, teamId, slug }) => { 72 | const url = new URL(`${BASE_URL}/v6/domains/${domain}/config`); 73 | if (strict !== undefined) url.searchParams.append("strict", String(strict)); 74 | if (teamId) url.searchParams.append("teamId", teamId); 75 | if (slug) url.searchParams.append("slug", slug); 76 | 77 | const response = await fetch(url.toString(), { 78 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 79 | }); 80 | 81 | const data = await handleResponse(response); 82 | return { 83 | content: [{ type: "text", text: `Domain configuration:\n${JSON.stringify(data, null, 2)}` }] 84 | }; 85 | } 86 | ); 87 | 88 | // Get domain registry info 89 | server.tool( 90 | "domain_registry", 91 | "Get domain transfer info", 92 | { 93 | domain: z.string().describe("The domain name"), 94 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 95 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 96 | }, 97 | async ({ domain, teamId, slug }) => { 98 | const url = new URL(`${BASE_URL}/v1/domains/${domain}/registry`); 99 | if (teamId) url.searchParams.append("teamId", teamId); 100 | if (slug) url.searchParams.append("slug", slug); 101 | 102 | const response = await fetch(url.toString(), { 103 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 104 | }); 105 | 106 | const data = await handleResponse(response); 107 | return { 108 | content: [{ type: "text", text: `Domain registry info:\n${JSON.stringify(data, null, 2)}` }] 109 | }; 110 | } 111 | ); 112 | 113 | // Get single domain info 114 | server.tool( 115 | "domain_get", 116 | "Get information for a single domain", 117 | { 118 | domain: z.string().describe("The name of the domain"), 119 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 120 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 121 | }, 122 | async ({ domain, teamId, slug }) => { 123 | const url = new URL(`${BASE_URL}/v5/domains/${domain}`); 124 | if (teamId) url.searchParams.append("teamId", teamId); 125 | if (slug) url.searchParams.append("slug", slug); 126 | 127 | const response = await fetch(url.toString(), { 128 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 129 | }); 130 | 131 | const data = await handleResponse(response); 132 | return { 133 | content: [{ type: "text", text: `Domain information:\n${JSON.stringify(data, null, 2)}` }] 134 | }; 135 | } 136 | ); 137 | 138 | // List domains 139 | server.tool( 140 | "domain_list", 141 | "List all domains", 142 | { 143 | limit: z.number().optional().describe("Maximum number of domains to list"), 144 | since: z.number().optional().describe("Get domains created after this timestamp"), 145 | until: z.number().optional().describe("Get domains created before this timestamp"), 146 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 147 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 148 | }, 149 | async ({ limit, since, until, teamId, slug }) => { 150 | const url = new URL(`${BASE_URL}/v5/domains`); 151 | if (limit) url.searchParams.append("limit", String(limit)); 152 | if (since) url.searchParams.append("since", String(since)); 153 | if (until) url.searchParams.append("until", String(until)); 154 | if (teamId) url.searchParams.append("teamId", teamId); 155 | if (slug) url.searchParams.append("slug", slug); 156 | 157 | const response = await fetch(url.toString(), { 158 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 159 | }); 160 | 161 | const data = await handleResponse(response); 162 | return { 163 | content: [{ type: "text", text: `Domains list:\n${JSON.stringify(data, null, 2)}` }] 164 | }; 165 | } 166 | ); 167 | 168 | // Purchase domain 169 | server.tool( 170 | "domain_buy", 171 | "Purchase a domain", 172 | { 173 | name: z.string().describe("The domain name to purchase"), 174 | expectedPrice: z.number().optional().describe("The expected price for the purchase"), 175 | renew: z.boolean().optional().describe("Whether to auto-renew the domain"), 176 | country: z.string().describe("The country of the domain registrant"), 177 | orgName: z.string().optional().describe("The company name of the domain registrant"), 178 | firstName: z.string().describe("The first name of the domain registrant"), 179 | lastName: z.string().describe("The last name of the domain registrant"), 180 | address1: z.string().describe("The street address of the domain registrant"), 181 | city: z.string().describe("The city of the domain registrant"), 182 | state: z.string().describe("The state of the domain registrant"), 183 | postalCode: z.string().describe("The postal code of the domain registrant"), 184 | phone: z.string().describe("The phone number of the domain registrant"), 185 | email: z.string().email().describe("The email of the domain registrant"), 186 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 187 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 188 | }, 189 | async ({ name, expectedPrice, renew, country, orgName, firstName, lastName, address1, city, state, postalCode, phone, email, teamId, slug }) => { 190 | const url = new URL(`${BASE_URL}/v5/domains/buy`); 191 | if (teamId) url.searchParams.append("teamId", teamId); 192 | if (slug) url.searchParams.append("slug", slug); 193 | 194 | const response = await fetch(url.toString(), { 195 | method: "POST", 196 | headers: { 197 | "Content-Type": "application/json", 198 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` 199 | }, 200 | body: JSON.stringify({ 201 | name, 202 | expectedPrice, 203 | renew, 204 | country, 205 | orgName, 206 | firstName, 207 | lastName, 208 | address1, 209 | city, 210 | state, 211 | postalCode, 212 | phone, 213 | email 214 | }) 215 | }); 216 | 217 | const data = await handleResponse(response); 218 | return { 219 | content: [{ type: "text", text: `Domain purchase result:\n${JSON.stringify(data, null, 2)}` }] 220 | }; 221 | } 222 | ); 223 | 224 | // Register/transfer domain 225 | server.tool( 226 | "domain_register", 227 | "Register or transfer-in a domain", 228 | { 229 | method: z.enum(["add", "transfer-in"]).describe("The domain operation to perform"), 230 | name: z.string().describe("The domain name"), 231 | cdnEnabled: z.boolean().optional().describe("Whether to enable CDN"), 232 | zone: z.boolean().optional().describe("Whether to create a zone"), 233 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 234 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 235 | }, 236 | async ({ method, name, cdnEnabled, zone, teamId, slug }) => { 237 | const url = new URL(`${BASE_URL}/v5/domains`); 238 | if (teamId) url.searchParams.append("teamId", teamId); 239 | if (slug) url.searchParams.append("slug", slug); 240 | 241 | const response = await fetch(url.toString(), { 242 | method: "POST", 243 | headers: { 244 | "Content-Type": "application/json", 245 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` 246 | }, 247 | body: JSON.stringify({ 248 | method, 249 | name, 250 | cdnEnabled, 251 | zone 252 | }) 253 | }); 254 | 255 | const data = await handleResponse(response); 256 | return { 257 | content: [{ type: "text", text: `Domain registration result:\n${JSON.stringify(data, null, 2)}` }] 258 | }; 259 | } 260 | ); 261 | 262 | // Remove domain 263 | server.tool( 264 | "domain_remove", 265 | "Remove a domain", 266 | { 267 | domain: z.string().describe("The name of the domain to remove"), 268 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 269 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 270 | }, 271 | async ({ domain, teamId, slug }) => { 272 | const url = new URL(`${BASE_URL}/v6/domains/${domain}`); 273 | if (teamId) url.searchParams.append("teamId", teamId); 274 | if (slug) url.searchParams.append("slug", slug); 275 | 276 | const response = await fetch(url.toString(), { 277 | method: "DELETE", 278 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 279 | }); 280 | 281 | const data = await handleResponse(response); 282 | return { 283 | content: [{ type: "text", text: `Domain removal result:\n${JSON.stringify(data, null, 2)}` }] 284 | }; 285 | } 286 | ); 287 | 288 | // Update domain 289 | server.tool( 290 | "domain_update", 291 | "Update or move apex domain", 292 | { 293 | domain: z.string().describe("The domain name"), 294 | op: z.enum(["update", "move-out"]).describe("Operation type"), 295 | renew: z.boolean().optional().describe("Whether to auto-renew"), 296 | customNameservers: z.array(z.string()).optional().describe("Custom nameservers"), 297 | zone: z.boolean().optional().describe("Whether to create a zone"), 298 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 299 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 300 | }, 301 | async ({ domain, op, renew, customNameservers, zone, teamId, slug }) => { 302 | const url = new URL(`${BASE_URL}/v3/domains/${domain}`); 303 | if (teamId) url.searchParams.append("teamId", teamId); 304 | if (slug) url.searchParams.append("slug", slug); 305 | 306 | const response = await fetch(url.toString(), { 307 | method: "PATCH", 308 | headers: { 309 | "Content-Type": "application/json", 310 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` 311 | }, 312 | body: JSON.stringify({ 313 | op, 314 | renew, 315 | customNameservers, 316 | zone 317 | }) 318 | }); 319 | 320 | const data = await handleResponse(response); 321 | return { 322 | content: [{ type: "text", text: `Domain update result:\n${JSON.stringify(data, null, 2)}` }] 323 | }; 324 | } 325 | ); 326 | } -------------------------------------------------------------------------------- /src/resources.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { BASE_URL, DEFAULT_ACCESS_TOKEN, handleResponse } from "./index.js"; 4 | 5 | export function registerResources(server: McpServer) { 6 | // Project resource 7 | server.resource( 8 | "project", 9 | new ResourceTemplate("projects://{projectId}", { list: undefined }), 10 | async (uri, variables) => { 11 | const response = await fetch(`${BASE_URL}/v9/projects/${variables.projectId}`, { 12 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 13 | }); 14 | const data = await handleResponse(response); 15 | return { 16 | contents: [{ 17 | uri: uri.href, 18 | text: JSON.stringify(data, null, 2) 19 | }] 20 | }; 21 | } 22 | ); 23 | 24 | // Team resource 25 | server.resource( 26 | "team", 27 | new ResourceTemplate("teams://{teamId}", { list: undefined }), 28 | async (uri, variables) => { 29 | const response = await fetch(`${BASE_URL}/v2/teams/${variables.teamId}`, { 30 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 31 | }); 32 | const data = await handleResponse(response); 33 | return { 34 | contents: [{ 35 | uri: uri.href, 36 | text: JSON.stringify(data, null, 2) 37 | }] 38 | }; 39 | } 40 | ); 41 | 42 | // Deployment resource 43 | server.resource( 44 | "deployment", 45 | new ResourceTemplate("deployments://{deploymentId}", { list: undefined }), 46 | async (uri, variables) => { 47 | const response = await fetch(`${BASE_URL}/v13/deployments/${variables.deploymentId}`, { 48 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 49 | }); 50 | const data = await handleResponse(response); 51 | return { 52 | contents: [{ 53 | uri: uri.href, 54 | text: JSON.stringify(data, null, 2) 55 | }] 56 | }; 57 | } 58 | ); 59 | 60 | // Environment Variables resource 61 | server.resource( 62 | "env-vars", 63 | new ResourceTemplate("env://{projectId}", { list: undefined }), 64 | async (uri, variables) => { 65 | const response = await fetch(`${BASE_URL}/v9/projects/${variables.projectId}/env`, { 66 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 67 | }); 68 | const data = await handleResponse(response); 69 | return { 70 | contents: [{ 71 | uri: uri.href, 72 | text: JSON.stringify(data, null, 2) 73 | }] 74 | }; 75 | } 76 | ); 77 | 78 | // Domains resource 79 | server.resource( 80 | "domains", 81 | new ResourceTemplate("domains://{domain}", { list: undefined }), 82 | async (uri, variables) => { 83 | const response = await fetch(`${BASE_URL}/v5/domains/${variables.domain}`, { 84 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 85 | }); 86 | const data = await handleResponse(response); 87 | return { 88 | contents: [{ 89 | uri: uri.href, 90 | text: JSON.stringify(data, null, 2) 91 | }] 92 | }; 93 | } 94 | ); 95 | 96 | // Webhook resource 97 | server.resource( 98 | "webhook", 99 | new ResourceTemplate("webhooks://{webhookId}", { list: undefined }), 100 | async (uri, variables) => { 101 | const response = await fetch(`${BASE_URL}/v1/webhooks/${variables.webhookId}`, { 102 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 103 | }); 104 | const data = await handleResponse(response); 105 | return { 106 | contents: [{ 107 | uri: uri.href, 108 | text: JSON.stringify(data, null, 2) 109 | }] 110 | }; 111 | } 112 | ); 113 | 114 | // User resource 115 | server.resource( 116 | "user", 117 | new ResourceTemplate("users://{userId}", { list: undefined }), 118 | async (uri, variables) => { 119 | const response = await fetch(`${BASE_URL}/v2/user`, { 120 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 121 | }); 122 | const data = await handleResponse(response); 123 | return { 124 | contents: [{ 125 | uri: uri.href, 126 | text: JSON.stringify(data, null, 2) 127 | }] 128 | }; 129 | } 130 | ); 131 | 132 | // Integration resource 133 | server.resource( 134 | "integration", 135 | new ResourceTemplate("integrations://{integrationId}", { list: undefined }), 136 | async (uri, variables) => { 137 | const response = await fetch(`${BASE_URL}/v1/integrations/${variables.integrationId}`, { 138 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 139 | }); 140 | const data = await handleResponse(response); 141 | return { 142 | contents: [{ 143 | uri: uri.href, 144 | text: JSON.stringify(data, null, 2) 145 | }] 146 | }; 147 | } 148 | ); 149 | 150 | // Project Member resource 151 | server.resource( 152 | "project-member", 153 | new ResourceTemplate("project-members://{projectId}/{userId}", { list: undefined }), 154 | async (uri, variables) => { 155 | const response = await fetch(`${BASE_URL}/v9/projects/${variables.projectId}/members/${variables.userId}`, { 156 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 157 | }); 158 | const data = await handleResponse(response); 159 | return { 160 | contents: [{ 161 | uri: uri.href, 162 | text: JSON.stringify(data, null, 2) 163 | }] 164 | }; 165 | } 166 | ); 167 | 168 | // Access Group resource 169 | server.resource( 170 | "access-group", 171 | new ResourceTemplate("access-groups://{groupId}", { list: undefined }), 172 | async (uri, variables) => { 173 | const response = await fetch(`${BASE_URL}/v1/access-groups/${variables.groupId}`, { 174 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 175 | }); 176 | const data = await handleResponse(response); 177 | return { 178 | contents: [{ 179 | uri: uri.href, 180 | text: JSON.stringify(data, null, 2) 181 | }] 182 | }; 183 | } 184 | ); 185 | 186 | // Log Drain resource 187 | server.resource( 188 | "log-drain", 189 | new ResourceTemplate("log-drains://{drainId}", { list: undefined }), 190 | async (uri, variables) => { 191 | const response = await fetch(`${BASE_URL}/v1/log-drains/${variables.drainId}`, { 192 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 193 | }); 194 | const data = await handleResponse(response); 195 | return { 196 | contents: [{ 197 | uri: uri.href, 198 | text: JSON.stringify(data, null, 2) 199 | }] 200 | }; 201 | } 202 | ); 203 | 204 | // Secret resource 205 | server.resource( 206 | "secret", 207 | new ResourceTemplate("secrets://{projectId}/{name}", { list: undefined }), 208 | async (uri, variables) => { 209 | const response = await fetch(`${BASE_URL}/v9/projects/${variables.projectId}/env/${variables.name}`, { 210 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 211 | }); 212 | const data = await handleResponse(response); 213 | return { 214 | contents: [{ 215 | uri: uri.href, 216 | text: JSON.stringify(data, null, 2) 217 | }] 218 | }; 219 | } 220 | ); 221 | 222 | // Alias resource 223 | server.resource( 224 | "alias", 225 | new ResourceTemplate("aliases://{aliasId}", { list: undefined }), 226 | async (uri, variables) => { 227 | const response = await fetch(`${BASE_URL}/v2/aliases/${variables.aliasId}`, { 228 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 229 | }); 230 | const data = await handleResponse(response); 231 | return { 232 | contents: [{ 233 | uri: uri.href, 234 | text: JSON.stringify(data, null, 2) 235 | }] 236 | }; 237 | } 238 | ); 239 | 240 | // Artifact resource 241 | server.resource( 242 | "artifact", 243 | new ResourceTemplate("artifacts://{projectId}/{artifactId}", { list: undefined }), 244 | async (uri, variables) => { 245 | const response = await fetch(`${BASE_URL}/v8/artifacts/${variables.projectId}/${variables.artifactId}`, { 246 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 247 | }); 248 | const data = await handleResponse(response); 249 | return { 250 | contents: [{ 251 | uri: uri.href, 252 | text: JSON.stringify(data, null, 2) 253 | }] 254 | }; 255 | } 256 | ); 257 | 258 | // Certificate resource 259 | server.resource( 260 | "certificate", 261 | new ResourceTemplate("certs://{certId}", { list: undefined }), 262 | async (uri, variables) => { 263 | const response = await fetch(`${BASE_URL}/v5/now/certs/${variables.certId}`, { 264 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 265 | }); 266 | const data = await handleResponse(response); 267 | return { 268 | contents: [{ 269 | uri: uri.href, 270 | text: JSON.stringify(data, null, 2) 271 | }] 272 | }; 273 | } 274 | ); 275 | 276 | // DNS resource 277 | server.resource( 278 | "dns", 279 | new ResourceTemplate("dns://{domain}/{recordId}", { list: undefined }), 280 | async (uri, variables) => { 281 | const response = await fetch(`${BASE_URL}/v2/domains/${variables.domain}/records/${variables.recordId}`, { 282 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 283 | }); 284 | const data = await handleResponse(response); 285 | return { 286 | contents: [{ 287 | uri: uri.href, 288 | text: JSON.stringify(data, null, 2) 289 | }] 290 | }; 291 | } 292 | ); 293 | 294 | // Marketplace resource 295 | server.resource( 296 | "marketplace", 297 | new ResourceTemplate("marketplace://{integration}", { list: undefined }), 298 | async (uri, variables) => { 299 | const response = await fetch(`${BASE_URL}/v1/marketplace/integrations/${variables.integration}`, { 300 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 301 | }); 302 | const data = await handleResponse(response); 303 | return { 304 | contents: [{ 305 | uri: uri.href, 306 | text: JSON.stringify(data, null, 2) 307 | }] 308 | }; 309 | } 310 | ); 311 | 312 | // Edge Config resource 313 | server.resource( 314 | "edge-config", 315 | new ResourceTemplate("edge-config://{configId}", { list: undefined }), 316 | async (uri, variables) => { 317 | const response = await fetch(`${BASE_URL}/v1/edge-config/${variables.configId}`, { 318 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 319 | }); 320 | const data = await handleResponse(response); 321 | return { 322 | contents: [{ 323 | uri: uri.href, 324 | text: JSON.stringify(data, null, 2) 325 | }] 326 | }; 327 | } 328 | ); 329 | 330 | // Speed Insights resource 331 | server.resource( 332 | "speed-insights", 333 | new ResourceTemplate("speed-insights://{projectId}", { list: undefined }), 334 | async (uri, variables) => { 335 | const response = await fetch(`${BASE_URL}/v1/speed-insights/${variables.projectId}`, { 336 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 337 | }); 338 | const data = await handleResponse(response); 339 | return { 340 | contents: [{ 341 | uri: uri.href, 342 | text: JSON.stringify(data, null, 2) 343 | }] 344 | }; 345 | } 346 | ); 347 | 348 | // Security resource 349 | server.resource( 350 | "security", 351 | new ResourceTemplate("security://{projectId}", { list: undefined }), 352 | async (uri, variables) => { 353 | const response = await fetch(`${BASE_URL}/v1/security/projects/${variables.projectId}`, { 354 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 355 | }); 356 | const data = await handleResponse(response); 357 | return { 358 | contents: [{ 359 | uri: uri.href, 360 | text: JSON.stringify(data, null, 2) 361 | }] 362 | }; 363 | } 364 | ); 365 | 366 | // Auth resource 367 | server.resource( 368 | "auth", 369 | new ResourceTemplate("auth://{token}", { list: undefined }), 370 | async (uri, variables) => { 371 | const response = await fetch(`${BASE_URL}/v2/user/tokens/${variables.token}`, { 372 | headers: { Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}` } 373 | }); 374 | const data = await handleResponse(response); 375 | return { 376 | contents: [{ 377 | uri: uri.href, 378 | text: JSON.stringify(data, null, 2) 379 | }] 380 | }; 381 | } 382 | ); 383 | 384 | // Static configuration resource 385 | server.resource( 386 | "config", 387 | "config://vercel", 388 | async (uri) => ({ 389 | contents: [{ 390 | uri: uri.href, 391 | text: JSON.stringify({ 392 | apiVersion: "v9", 393 | baseUrl: BASE_URL, 394 | defaultTeam: null, 395 | features: { 396 | deployments: true, 397 | teams: true, 398 | domains: true, 399 | envVars: true, 400 | analytics: true, 401 | webhooks: true, 402 | users: true, 403 | integrations: true, 404 | projectMembers: true, 405 | accessGroups: true, 406 | logDrains: true, 407 | secrets: true, 408 | aliases: true, 409 | artifacts: true, 410 | certificates: true, 411 | dns: true, 412 | marketplace: true, 413 | edgeConfig: true, 414 | speedInsights: true, 415 | security: true, 416 | auth: true 417 | } 418 | }, null, 2) 419 | }] 420 | }) 421 | ); 422 | } -------------------------------------------------------------------------------- /src/components/edge-config.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { handleResponse } from "../utils/response.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { BASE_URL, DEFAULT_ACCESS_TOKEN } from "../config/constants.js"; 5 | 6 | // Common schemas 7 | const EdgeConfigItemSchema = z.object({ 8 | key: z.string(), 9 | value: z.any(), 10 | description: z.string().optional() 11 | }); 12 | 13 | const EdgeConfigTokenSchema = z.object({ 14 | token: z.string(), 15 | label: z.string(), 16 | id: z.string(), 17 | edgeConfigId: z.string(), 18 | createdAt: z.number() 19 | }); 20 | 21 | const EdgeConfigSchema = z.object({ 22 | createdAt: z.number(), 23 | updatedAt: z.number(), 24 | id: z.string(), 25 | slug: z.string(), 26 | ownerId: z.string(), 27 | digest: z.string(), 28 | transfer: z.object({ 29 | fromAccountId: z.string(), 30 | startedAt: z.number(), 31 | doneAt: z.null() 32 | }).optional(), 33 | schema: z.record(z.any()), 34 | purpose: z.object({ 35 | type: z.literal("flags"), 36 | projectId: z.string() 37 | }).optional(), 38 | sizeInBytes: z.number(), 39 | itemCount: z.number() 40 | }); 41 | 42 | export function registerEdgeConfigTools(server: McpServer) { 43 | // Create Edge Config 44 | server.tool( 45 | "create_edge_config", 46 | "Create a new Edge Config", 47 | { 48 | slug: z.string().max(64).regex(/^[\w-]+$/).describe("Edge Config slug"), 49 | items: z.record(z.any()).optional().describe("Initial items") 50 | }, 51 | async (body) => { 52 | const response = await fetch(`${BASE_URL}/v1/edge-config`, { 53 | method: "POST", 54 | headers: { 55 | "Content-Type": "application/json", 56 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 57 | }, 58 | body: JSON.stringify(body), 59 | }); 60 | 61 | const data = await handleResponse(response); 62 | return { 63 | content: [{ type: "text", text: `Edge Config created:\n${JSON.stringify(data, null, 2)}` }], 64 | }; 65 | } 66 | ); 67 | 68 | // Create Edge Config Token 69 | server.tool( 70 | "create_edge_config_token", 71 | "Create a new Edge Config Token", 72 | { 73 | edgeConfigId: z.string().describe("Edge Config ID"), 74 | label: z.string().max(52).describe("Token label") 75 | }, 76 | async ({ edgeConfigId, label }) => { 77 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/token`, { 78 | method: "POST", 79 | headers: { 80 | "Content-Type": "application/json", 81 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 82 | }, 83 | body: JSON.stringify({ label }), 84 | }); 85 | 86 | const data = await handleResponse(response); 87 | return { 88 | content: [{ type: "text", text: `Token created:\n${JSON.stringify(data, null, 2)}` }], 89 | }; 90 | } 91 | ); 92 | 93 | // Get Edge Configs 94 | server.tool( 95 | "list_edge_configs", 96 | "List all Edge Configs", 97 | {}, 98 | async () => { 99 | const response = await fetch(`${BASE_URL}/v1/edge-config`, { 100 | headers: { 101 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 102 | }, 103 | }); 104 | 105 | const data = await handleResponse(response); 106 | return { 107 | content: [{ type: "text", text: `Edge Configs:\n${JSON.stringify(data, null, 2)}` }], 108 | }; 109 | } 110 | ); 111 | 112 | // Get Edge Config 113 | server.tool( 114 | "get_edge_config", 115 | "Get an Edge Config", 116 | { 117 | edgeConfigId: z.string().describe("Edge Config ID") 118 | }, 119 | async ({ edgeConfigId }) => { 120 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}`, { 121 | headers: { 122 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 123 | }, 124 | }); 125 | 126 | const data = await handleResponse(response); 127 | return { 128 | content: [{ type: "text", text: `Edge Config details:\n${JSON.stringify(data, null, 2)}` }], 129 | }; 130 | } 131 | ); 132 | 133 | // Update Edge Config 134 | server.tool( 135 | "update_edge_config", 136 | "Update an Edge Config", 137 | { 138 | edgeConfigId: z.string().describe("Edge Config ID"), 139 | slug: z.string().max(64).regex(/^[\w-]+$/).describe("New slug") 140 | }, 141 | async ({ edgeConfigId, slug }) => { 142 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}`, { 143 | method: "PUT", 144 | headers: { 145 | "Content-Type": "application/json", 146 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 147 | }, 148 | body: JSON.stringify({ slug }), 149 | }); 150 | 151 | const data = await handleResponse(response); 152 | return { 153 | content: [{ type: "text", text: `Edge Config updated:\n${JSON.stringify(data, null, 2)}` }], 154 | }; 155 | } 156 | ); 157 | 158 | // Delete Edge Config 159 | server.tool( 160 | "delete_edge_config", 161 | "Delete an Edge Config", 162 | { 163 | edgeConfigId: z.string().describe("Edge Config ID") 164 | }, 165 | async ({ edgeConfigId }) => { 166 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}`, { 167 | method: "DELETE", 168 | headers: { 169 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 170 | }, 171 | }); 172 | 173 | await handleResponse(response); 174 | return { 175 | content: [{ type: "text", text: "Edge Config deleted successfully" }], 176 | }; 177 | } 178 | ); 179 | 180 | // Get Edge Config Items 181 | server.tool( 182 | "list_edge_config_items", 183 | "List Edge Config Items", 184 | { 185 | edgeConfigId: z.string().describe("Edge Config ID") 186 | }, 187 | async ({ edgeConfigId }) => { 188 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/items`, { 189 | headers: { 190 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 191 | }, 192 | }); 193 | 194 | const data = await handleResponse(response); 195 | return { 196 | content: [{ type: "text", text: `Edge Config items:\n${JSON.stringify(data, null, 2)}` }], 197 | }; 198 | } 199 | ); 200 | 201 | // Get Edge Config Item 202 | server.tool( 203 | "get_edge_config_item", 204 | "Get an Edge Config Item", 205 | { 206 | edgeConfigId: z.string().describe("Edge Config ID"), 207 | itemKey: z.string().describe("Item key") 208 | }, 209 | async ({ edgeConfigId, itemKey }) => { 210 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/item/${itemKey}`, { 211 | headers: { 212 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 213 | }, 214 | }); 215 | 216 | const data = await handleResponse(response); 217 | return { 218 | content: [{ type: "text", text: `Item details:\n${JSON.stringify(data, null, 2)}` }], 219 | }; 220 | } 221 | ); 222 | 223 | // Update Edge Config Items 224 | server.tool( 225 | "update_edge_config_items", 226 | "Update Edge Config Items", 227 | { 228 | edgeConfigId: z.string().describe("Edge Config ID"), 229 | items: z.array(z.object({ 230 | operation: z.enum(["upsert", "remove"]), 231 | key: z.string(), 232 | value: z.any().optional(), 233 | description: z.string().optional() 234 | })).describe("Items to update"), 235 | definition: z.any().optional().describe("Schema definition") 236 | }, 237 | async ({ edgeConfigId, ...body }) => { 238 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/items`, { 239 | method: "PATCH", 240 | headers: { 241 | "Content-Type": "application/json", 242 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 243 | }, 244 | body: JSON.stringify(body), 245 | }); 246 | 247 | const data = await handleResponse(response); 248 | return { 249 | content: [{ type: "text", text: `Items updated:\n${JSON.stringify(data, null, 2)}` }], 250 | }; 251 | } 252 | ); 253 | 254 | // Get Edge Config Schema 255 | server.tool( 256 | "get_edge_config_schema", 257 | "Get Edge Config Schema", 258 | { 259 | edgeConfigId: z.string().describe("Edge Config ID") 260 | }, 261 | async ({ edgeConfigId }) => { 262 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/schema`, { 263 | headers: { 264 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 265 | }, 266 | }); 267 | 268 | const data = await handleResponse(response); 269 | return { 270 | content: [{ type: "text", text: `Schema:\n${JSON.stringify(data, null, 2)}` }], 271 | }; 272 | } 273 | ); 274 | 275 | // Update Edge Config Schema 276 | server.tool( 277 | "update_edge_config_schema", 278 | "Update Edge Config Schema", 279 | { 280 | edgeConfigId: z.string().describe("Edge Config ID"), 281 | definition: z.any().describe("Schema definition") 282 | }, 283 | async ({ edgeConfigId, definition }) => { 284 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/schema`, { 285 | method: "POST", 286 | headers: { 287 | "Content-Type": "application/json", 288 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 289 | }, 290 | body: JSON.stringify({ definition }), 291 | }); 292 | 293 | const data = await handleResponse(response); 294 | return { 295 | content: [{ type: "text", text: `Schema updated:\n${JSON.stringify(data, null, 2)}` }], 296 | }; 297 | } 298 | ); 299 | 300 | // Delete Edge Config Schema 301 | server.tool( 302 | "delete_edge_config_schema", 303 | "Delete Edge Config Schema", 304 | { 305 | edgeConfigId: z.string().describe("Edge Config ID") 306 | }, 307 | async ({ edgeConfigId }) => { 308 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/schema`, { 309 | method: "DELETE", 310 | headers: { 311 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 312 | }, 313 | }); 314 | 315 | await handleResponse(response); 316 | return { 317 | content: [{ type: "text", text: "Schema deleted successfully" }], 318 | }; 319 | } 320 | ); 321 | 322 | // Get Edge Config Tokens 323 | server.tool( 324 | "list_edge_config_tokens", 325 | "List Edge Config Tokens", 326 | { 327 | edgeConfigId: z.string().describe("Edge Config ID") 328 | }, 329 | async ({ edgeConfigId }) => { 330 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/tokens`, { 331 | headers: { 332 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 333 | }, 334 | }); 335 | 336 | const data = await handleResponse(response); 337 | return { 338 | content: [{ type: "text", text: `Tokens:\n${JSON.stringify(data, null, 2)}` }], 339 | }; 340 | } 341 | ); 342 | 343 | // Get Edge Config Token 344 | server.tool( 345 | "get_edge_config_token", 346 | "Get Edge Config Token", 347 | { 348 | edgeConfigId: z.string().describe("Edge Config ID"), 349 | token: z.string().describe("Token value") 350 | }, 351 | async ({ edgeConfigId, token }) => { 352 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/token/${token}`, { 353 | headers: { 354 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 355 | }, 356 | }); 357 | 358 | const data = await handleResponse(response); 359 | return { 360 | content: [{ type: "text", text: `Token details:\n${JSON.stringify(data, null, 2)}` }], 361 | }; 362 | } 363 | ); 364 | 365 | // Delete Edge Config Tokens 366 | server.tool( 367 | "delete_edge_config_tokens", 368 | "Delete Edge Config Tokens", 369 | { 370 | edgeConfigId: z.string().describe("Edge Config ID"), 371 | tokens: z.array(z.string()).describe("Tokens to delete") 372 | }, 373 | async ({ edgeConfigId, tokens }) => { 374 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/tokens`, { 375 | method: "DELETE", 376 | headers: { 377 | "Content-Type": "application/json", 378 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 379 | }, 380 | body: JSON.stringify({ tokens }), 381 | }); 382 | 383 | await handleResponse(response); 384 | return { 385 | content: [{ type: "text", text: "Tokens deleted successfully" }], 386 | }; 387 | } 388 | ); 389 | 390 | // Get Edge Config Backups 391 | server.tool( 392 | "list_edge_config_backups", 393 | "List Edge Config Backups", 394 | { 395 | edgeConfigId: z.string().describe("Edge Config ID"), 396 | limit: z.number().min(0).max(50).optional().describe("Number of backups to return"), 397 | next: z.string().optional().describe("Next page token") 398 | }, 399 | async ({ edgeConfigId, ...params }) => { 400 | const queryParams = new URLSearchParams(); 401 | if (params.limit) queryParams.set("limit", params.limit.toString()); 402 | if (params.next) queryParams.set("next", params.next); 403 | 404 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/backups?${queryParams}`, { 405 | headers: { 406 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 407 | }, 408 | }); 409 | 410 | const data = await handleResponse(response); 411 | return { 412 | content: [{ type: "text", text: `Backups:\n${JSON.stringify(data, null, 2)}` }], 413 | }; 414 | } 415 | ); 416 | 417 | // Get Edge Config Backup 418 | server.tool( 419 | "get_edge_config_backup", 420 | "Get Edge Config Backup", 421 | { 422 | edgeConfigId: z.string().describe("Edge Config ID"), 423 | backupId: z.string().describe("Backup version ID") 424 | }, 425 | async ({ edgeConfigId, backupId }) => { 426 | const response = await fetch(`${BASE_URL}/v1/edge-config/${edgeConfigId}/backups/${backupId}`, { 427 | headers: { 428 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 429 | }, 430 | }); 431 | 432 | const data = await handleResponse(response); 433 | return { 434 | content: [{ type: "text", text: `Backup details:\n${JSON.stringify(data, null, 2)}` }], 435 | }; 436 | } 437 | ); 438 | } -------------------------------------------------------------------------------- /src/components/accessgroups.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | import { handleResponse, BASE_URL, DEFAULT_ACCESS_TOKEN } from "../index.js"; 4 | 5 | // Common parameter schemas 6 | const teamParams = { 7 | teamId: z.string().optional().describe("The Team identifier to perform the request on behalf of"), 8 | slug: z.string().optional().describe("The Team slug to perform the request on behalf of") 9 | }; 10 | 11 | const projectRoleEnum = z.enum(["ADMIN", "PROJECT_VIEWER", "PROJECT_DEVELOPER"]); 12 | 13 | export function registerAccessGroupTools(server: McpServer) { 14 | // Create access group project 15 | server.tool( 16 | "create_access_group_project", 17 | "Create an access group project", 18 | { 19 | accessGroupIdOrName: z.string().describe("The access group ID or name"), 20 | projectId: z.string().max(256).describe("The ID of the project"), 21 | role: projectRoleEnum.describe("The project role that will be added to this Access Group"), 22 | ...teamParams 23 | }, 24 | async ({ accessGroupIdOrName, projectId, role, teamId, slug }) => { 25 | const url = new URL(`${BASE_URL}/v1/access-groups/${accessGroupIdOrName}/projects`); 26 | if (teamId) url.searchParams.append("teamId", teamId); 27 | if (slug) url.searchParams.append("slug", slug); 28 | 29 | const response = await fetch(url.toString(), { 30 | method: "POST", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 34 | }, 35 | body: JSON.stringify({ projectId, role }), 36 | }); 37 | 38 | const data = await handleResponse(response); 39 | return { 40 | content: [{ type: "text", text: `Access group project created:\n${JSON.stringify(data, null, 2)}` }], 41 | }; 42 | } 43 | ); 44 | 45 | // Create access group 46 | server.tool( 47 | "create_access_group", 48 | "Create a new access group", 49 | { 50 | name: z.string().max(50).regex(/^[A-z0-9_ -]+$/).describe("The name of the access group"), 51 | projects: z.array(z.object({ 52 | projectId: z.string(), 53 | role: projectRoleEnum 54 | })).optional().describe("List of projects to add to the access group"), 55 | membersToAdd: z.array(z.string()).optional().describe("List of members to add to the access group"), 56 | ...teamParams 57 | }, 58 | async ({ name, projects, membersToAdd, teamId, slug }) => { 59 | const url = new URL(`${BASE_URL}/v1/access-groups`); 60 | if (teamId) url.searchParams.append("teamId", teamId); 61 | if (slug) url.searchParams.append("slug", slug); 62 | 63 | const response = await fetch(url.toString(), { 64 | method: "POST", 65 | headers: { 66 | "Content-Type": "application/json", 67 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 68 | }, 69 | body: JSON.stringify({ 70 | name, 71 | ...(projects && { projects }), 72 | ...(membersToAdd && { membersToAdd }) 73 | }), 74 | }); 75 | 76 | const data = await handleResponse(response); 77 | return { 78 | content: [{ type: "text", text: `Access group created:\n${JSON.stringify(data, null, 2)}` }], 79 | }; 80 | } 81 | ); 82 | 83 | // Delete access group project 84 | server.tool( 85 | "delete_access_group_project", 86 | "Delete an access group project", 87 | { 88 | accessGroupIdOrName: z.string().describe("The access group ID or name"), 89 | projectId: z.string().describe("The project ID"), 90 | ...teamParams 91 | }, 92 | async ({ accessGroupIdOrName, projectId, teamId, slug }) => { 93 | const url = new URL(`${BASE_URL}/v1/access-groups/${accessGroupIdOrName}/projects/${projectId}`); 94 | if (teamId) url.searchParams.append("teamId", teamId); 95 | if (slug) url.searchParams.append("slug", slug); 96 | 97 | const response = await fetch(url.toString(), { 98 | method: "DELETE", 99 | headers: { 100 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 101 | }, 102 | }); 103 | 104 | await handleResponse(response); 105 | return { 106 | content: [{ type: "text", text: "Access group project deleted successfully" }], 107 | }; 108 | } 109 | ); 110 | 111 | // Delete access group 112 | server.tool( 113 | "delete_access_group", 114 | "Delete an access group", 115 | { 116 | idOrName: z.string().describe("The access group ID or name"), 117 | ...teamParams 118 | }, 119 | async ({ idOrName, teamId, slug }) => { 120 | const url = new URL(`${BASE_URL}/v1/access-groups/${idOrName}`); 121 | if (teamId) url.searchParams.append("teamId", teamId); 122 | if (slug) url.searchParams.append("slug", slug); 123 | 124 | const response = await fetch(url.toString(), { 125 | method: "DELETE", 126 | headers: { 127 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 128 | }, 129 | }); 130 | 131 | await handleResponse(response); 132 | return { 133 | content: [{ type: "text", text: "Access group deleted successfully" }], 134 | }; 135 | } 136 | ); 137 | 138 | // List access groups 139 | server.tool( 140 | "list_access_groups", 141 | "List access groups for a team, project or member", 142 | { 143 | projectId: z.string().optional().describe("Filter access groups by project"), 144 | search: z.string().optional().describe("Search for access groups by name"), 145 | membersLimit: z.number().min(1).max(100).optional().describe("Number of members to include in the response"), 146 | projectsLimit: z.number().min(1).max(100).optional().describe("Number of projects to include in the response"), 147 | limit: z.number().min(1).max(100).optional().describe("Limit how many access group should be returned"), 148 | next: z.string().optional().describe("Continuation cursor to retrieve the next page of results"), 149 | ...teamParams 150 | }, 151 | async ({ projectId, search, membersLimit, projectsLimit, limit, next, teamId, slug }) => { 152 | const url = new URL(`${BASE_URL}/v1/access-groups`); 153 | const queryParams = new URLSearchParams(); 154 | 155 | if (projectId) queryParams.append("projectId", projectId); 156 | if (search) queryParams.append("search", search); 157 | if (membersLimit) queryParams.append("membersLimit", membersLimit.toString()); 158 | if (projectsLimit) queryParams.append("projectsLimit", projectsLimit.toString()); 159 | if (limit) queryParams.append("limit", limit.toString()); 160 | if (next) queryParams.append("next", next); 161 | if (teamId) queryParams.append("teamId", teamId); 162 | if (slug) queryParams.append("slug", slug); 163 | 164 | const response = await fetch(`${url.toString()}?${queryParams.toString()}`, { 165 | headers: { 166 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 167 | }, 168 | }); 169 | 170 | const data = await handleResponse(response); 171 | return { 172 | content: [{ type: "text", text: `Access groups:\n${JSON.stringify(data, null, 2)}` }], 173 | }; 174 | } 175 | ); 176 | 177 | // List access group members 178 | server.tool( 179 | "list_access_group_members", 180 | "List members of an access group", 181 | { 182 | idOrName: z.string().describe("The ID or name of the Access Group"), 183 | limit: z.number().min(1).max(100).optional().describe("Limit how many access group members should be returned"), 184 | next: z.string().optional().describe("Continuation cursor to retrieve the next page of results"), 185 | search: z.string().optional().describe("Search project members by their name, username, and email"), 186 | ...teamParams 187 | }, 188 | async ({ idOrName, limit, next, search, teamId, slug }) => { 189 | const url = new URL(`${BASE_URL}/v1/access-groups/${idOrName}/members`); 190 | const queryParams = new URLSearchParams(); 191 | 192 | if (limit) queryParams.append("limit", limit.toString()); 193 | if (next) queryParams.append("next", next); 194 | if (search) queryParams.append("search", search); 195 | if (teamId) queryParams.append("teamId", teamId); 196 | if (slug) queryParams.append("slug", slug); 197 | 198 | const response = await fetch(`${url.toString()}?${queryParams.toString()}`, { 199 | headers: { 200 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 201 | }, 202 | }); 203 | 204 | const data = await handleResponse(response); 205 | return { 206 | content: [{ type: "text", text: `Access group members:\n${JSON.stringify(data, null, 2)}` }], 207 | }; 208 | } 209 | ); 210 | 211 | // List access group projects 212 | server.tool( 213 | "list_access_group_projects", 214 | "List projects of an access group", 215 | { 216 | idOrName: z.string().describe("The ID or name of the Access Group"), 217 | limit: z.number().min(1).max(100).optional().describe("Limit how many access group projects should be returned"), 218 | next: z.string().optional().describe("Continuation cursor to retrieve the next page of results"), 219 | ...teamParams 220 | }, 221 | async ({ idOrName, limit, next, teamId, slug }) => { 222 | const url = new URL(`${BASE_URL}/v1/access-groups/${idOrName}/projects`); 223 | const queryParams = new URLSearchParams(); 224 | 225 | if (limit) queryParams.append("limit", limit.toString()); 226 | if (next) queryParams.append("next", next); 227 | if (teamId) queryParams.append("teamId", teamId); 228 | if (slug) queryParams.append("slug", slug); 229 | 230 | const response = await fetch(`${url.toString()}?${queryParams.toString()}`, { 231 | headers: { 232 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 233 | }, 234 | }); 235 | 236 | const data = await handleResponse(response); 237 | return { 238 | content: [{ type: "text", text: `Access group projects:\n${JSON.stringify(data, null, 2)}` }], 239 | }; 240 | } 241 | ); 242 | 243 | // Get access group 244 | server.tool( 245 | "get_access_group", 246 | "Read an access group", 247 | { 248 | idOrName: z.string().describe("The access group ID or name"), 249 | ...teamParams 250 | }, 251 | async ({ idOrName, teamId, slug }) => { 252 | const url = new URL(`${BASE_URL}/v1/access-groups/${idOrName}`); 253 | if (teamId) url.searchParams.append("teamId", teamId); 254 | if (slug) url.searchParams.append("slug", slug); 255 | 256 | const response = await fetch(url.toString(), { 257 | headers: { 258 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 259 | }, 260 | }); 261 | 262 | const data = await handleResponse(response); 263 | return { 264 | content: [{ type: "text", text: `Access group details:\n${JSON.stringify(data, null, 2)}` }], 265 | }; 266 | } 267 | ); 268 | 269 | // Get access group project 270 | server.tool( 271 | "get_access_group_project", 272 | "Read an access group project", 273 | { 274 | accessGroupIdOrName: z.string().describe("The access group ID or name"), 275 | projectId: z.string().describe("The project ID"), 276 | ...teamParams 277 | }, 278 | async ({ accessGroupIdOrName, projectId, teamId, slug }) => { 279 | const url = new URL(`${BASE_URL}/v1/access-groups/${accessGroupIdOrName}/projects/${projectId}`); 280 | if (teamId) url.searchParams.append("teamId", teamId); 281 | if (slug) url.searchParams.append("slug", slug); 282 | 283 | const response = await fetch(url.toString(), { 284 | headers: { 285 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 286 | }, 287 | }); 288 | 289 | const data = await handleResponse(response); 290 | return { 291 | content: [{ type: "text", text: `Access group project details:\n${JSON.stringify(data, null, 2)}` }], 292 | }; 293 | } 294 | ); 295 | 296 | // Update access group 297 | server.tool( 298 | "update_access_group", 299 | "Update an access group", 300 | { 301 | idOrName: z.string().describe("The access group ID or name"), 302 | name: z.string().max(50).regex(/^[A-z0-9_ -]+$/).optional().describe("The name of the access group"), 303 | projects: z.array(z.object({ 304 | projectId: z.string(), 305 | role: projectRoleEnum 306 | })).optional().describe("List of projects to update"), 307 | membersToAdd: z.array(z.string()).optional().describe("List of members to add to the access group"), 308 | membersToRemove: z.array(z.string()).optional().describe("List of members to remove from the access group"), 309 | ...teamParams 310 | }, 311 | async ({ idOrName, name, projects, membersToAdd, membersToRemove, teamId, slug }) => { 312 | const url = new URL(`${BASE_URL}/v1/access-groups/${idOrName}`); 313 | if (teamId) url.searchParams.append("teamId", teamId); 314 | if (slug) url.searchParams.append("slug", slug); 315 | 316 | const response = await fetch(url.toString(), { 317 | method: "POST", 318 | headers: { 319 | "Content-Type": "application/json", 320 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 321 | }, 322 | body: JSON.stringify({ 323 | ...(name && { name }), 324 | ...(projects && { projects }), 325 | ...(membersToAdd && { membersToAdd }), 326 | ...(membersToRemove && { membersToRemove }) 327 | }), 328 | }); 329 | 330 | const data = await handleResponse(response); 331 | return { 332 | content: [{ type: "text", text: `Access group updated:\n${JSON.stringify(data, null, 2)}` }], 333 | }; 334 | } 335 | ); 336 | 337 | // Update access group project 338 | server.tool( 339 | "update_access_group_project", 340 | "Update an access group project", 341 | { 342 | accessGroupIdOrName: z.string().describe("The access group ID or name"), 343 | projectId: z.string().describe("The project ID"), 344 | role: projectRoleEnum.describe("The project role that will be added to this Access Group"), 345 | ...teamParams 346 | }, 347 | async ({ accessGroupIdOrName, projectId, role, teamId, slug }) => { 348 | const url = new URL(`${BASE_URL}/v1/access-groups/${accessGroupIdOrName}/projects/${projectId}`); 349 | if (teamId) url.searchParams.append("teamId", teamId); 350 | if (slug) url.searchParams.append("slug", slug); 351 | 352 | const response = await fetch(url.toString(), { 353 | method: "PATCH", 354 | headers: { 355 | "Content-Type": "application/json", 356 | Authorization: `Bearer ${DEFAULT_ACCESS_TOKEN}`, 357 | }, 358 | body: JSON.stringify({ role }), 359 | }); 360 | 361 | const data = await handleResponse(response); 362 | return { 363 | content: [{ type: "text", text: `Access group project updated:\n${JSON.stringify(data, null, 2)}` }], 364 | }; 365 | } 366 | ); 367 | } --------------------------------------------------------------------------------