Skip to main content
Glama
netlify

Netlify MCP Server

Official
by netlify
index.ts6.82 kB
// coding context // focus tools on domain + focused operations // tool domains: // [] site (includes builds, domains, forms, env vars, settings,) // [] builds - why did it fail // [] domains // [x] forms // [x] env vars // [x] access controls // [] settings // [] deploy // [x] build + deploy // [] rollback/deploy selection // [x] user-and-team // [x] user // [x] team // [] team env vars // [] sites aggregate operations // [x] extensions - install and link - not configuration // [] database // // return errors when missing data and how the agent can get the data import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { userDomainTools } from './user-tools/index.js'; import { deployDomainTools } from './deploy-tools/index.js'; import { teamDomainTools } from './team-tools/index.js'; import { projectDomainTools } from './project-tools/index.js'; import { extensionDomainTools } from './extension-tools/index.js'; import { checkCompatibility } from '../utils/compatibility.js'; import { getNetlifyAccessToken, NetlifyUnauthError } from '../utils/api-networking.js'; import { appendToLog } from '../utils/logging.js'; import { categorizeToolsByReadWrite } from './tool-utils.js'; import { z } from 'zod'; import type { DomainTool } from './types.js'; const listOfDomainTools = [userDomainTools, deployDomainTools, teamDomainTools, projectDomainTools, extensionDomainTools]; const toSelectorSchema = (domainTool: DomainTool<any>) => { return z.object({ // domain: z.literal(domainTool.domain), operation: z.literal(domainTool.operation), params: domainTool.inputSchema.optional(), llmModelName: z.string().optional(), aiAgentName: z.string().optional() }); } export const bindTools = async (server: McpServer, remoteMCPRequest?: Request, verboseMode: boolean = false) => { const isRemoteMCP = !!remoteMCPRequest; listOfDomainTools.forEach(domainTools => { const domain = domainTools[0].domain; const filteredDomainTools = domainTools.filter(tool => { if(isRemoteMCP && tool.omitFromRemoteMCP) { return false; } if(!isRemoteMCP && tool.omitFromLocalMCP) { return false; } return true; }); // Categorize tools into read-only and write operations const { readOnlyTools, writeTools } = categorizeToolsByReadWrite(filteredDomainTools); // Register read-only tools if any exist if (readOnlyTools.length > 0) { registerDomainTools(server, readOnlyTools, domain, 'read', remoteMCPRequest, verboseMode); } // Register write tools if any exist if (writeTools.length > 0) { registerDomainTools(server, writeTools, domain, 'write', remoteMCPRequest, verboseMode); } }); }; const registerDomainTools = ( server: McpServer, tools: DomainTool<any>[], domain: string, operationType: 'read' | 'write', remoteMCPRequest?: Request, verboseMode: boolean = false ) => { const toolOperations = tools.map(tool => tool.operation); const readOnlyIndicator = operationType === 'read' ? ' (read-only)' : ''; if (verboseMode) { // Register each tool individually (no anyOf/union) tools.forEach(tool => { const toolName = `netlify-${domain}-${tool.operation}`; const toolDescription = `${tool.operation} operation for Netlify ${domain}${readOnlyIndicator}`; // The MCP SDK expects inputSchema to be a plain object with Zod schemas as properties // We need to extract the shape from the Zod object and use it directly const schemaShape = tool.inputSchema.shape || {}; server.registerTool(toolName, { description: toolDescription, inputSchema: schemaShape, annotations: { readOnlyHint: operationType === 'read' } }, async (...args) => { checkCompatibility(); try { await getNetlifyAccessToken(remoteMCPRequest); } catch (error: NetlifyUnauthError | any) { if (error instanceof NetlifyUnauthError && remoteMCPRequest) { throw new NetlifyUnauthError(); } return { content: [{ type: "text", text: error?.message || 'Failed to get Netlify token' }], isError: true }; } appendToLog(`${toolName} operation: ${JSON.stringify(args)}`); const result = await tool.cb(args[0], {request: remoteMCPRequest, isRemoteMCP: !!remoteMCPRequest}); appendToLog(`${domain} operation result: ${JSON.stringify(result)}`); return { content: [{ type: "text", text: JSON.stringify(result) }] } }); }); } else { // Register tools grouped by domain with selector (uses anyOf/union) const paramsSchema = { // @ts-ignore selectSchema: tools.length > 1 ? z.union(tools.map(tool => toSelectorSchema(tool))) : toSelectorSchema(tools[0]) }; const friendlyOperationType = operationType === 'read' ? 'reader' : 'updater'; const toolName = `netlify-${domain}-services-${friendlyOperationType}`; const toolDescription = `Select and run one of the following Netlify ${operationType} operations${readOnlyIndicator} ${toolOperations.join(', ')}`; server.registerTool(toolName, { description: toolDescription, inputSchema: paramsSchema, annotations: { readOnlyHint: operationType === 'read' } }, async (...args) => { checkCompatibility(); try { await getNetlifyAccessToken(remoteMCPRequest); } catch (error: NetlifyUnauthError | any) { if (error instanceof NetlifyUnauthError && remoteMCPRequest) { throw new NetlifyUnauthError(); } return { content: [{ type: "text", text: error?.message || 'Failed to get Netlify token' }], isError: true }; } appendToLog(`${toolName} operation: ${JSON.stringify(args)}`); const selectedSchema = args[0]?.selectSchema as any; if (!selectedSchema) { return { content: [{ type: "text", text: 'Failed to select a valid operation. Retry the MCP operation but select the operation and provide the right inputs.' }] } } const operation = selectedSchema.operation; const subtool = tools.find(subtool => subtool.operation === operation); if (!subtool) { return { content: [{ type: "text", text: 'Agent called the wrong MCP tool for this operation.' }] } } const result = await subtool.cb(selectedSchema.params || {}, {request: remoteMCPRequest, isRemoteMCP: !!remoteMCPRequest}); appendToLog(`${domain} operation result: ${JSON.stringify(result)}`); return { content: [{ type: "text", text: JSON.stringify(result) }] } }); } };

Implementation Reference

Latest Blog Posts

MCP directory API

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

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

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