Skip to main content
Glama
netlify

Netlify MCP Server

Official
by netlify
api-networking.ts7.83 kB
import path from 'node:path'; import * as fs from 'node:fs/promises'; import envPaths from 'env-paths'; import { runCommand } from './cmd.js'; import { appendToLog } from './logging.js'; import { decryptJWE } from '../../netlify/functions/mcp-server/utils.js'; interface APIInteractionOptions { pagination?: boolean; pageSize?: number; pageLimit?: number; pageOffset?: number; failureCallback?: (response: Response) => string | void; } const getAuthTokenMsg = ` You're not logged into Netlify on this computer. Use the netlify cli to login. \`netlify login\` If you don't have the netlify cli installed, install it by running "npm i -g netlify-cli", ` export const UNAUTHED_ERROR_PREFIX = 'NetlifyUnauthError:'; export class NetlifyUnauthError extends Error { constructor(message?: string) { super(`${UNAUTHED_ERROR_PREFIX} ${message || 'unauthenticated request to Netlify MCP API'}`); this.name = 'NetlifyUnauthError'; } } const readTokenFromEnv = async () => { try { // Netlify CLI uses envPaths(...) to build the file path for config.json. // https://github.com/netlify/cli/blob/f10fb055ab47bb8e7e2021bdfa955ce6733d5041/src/lib/settings.ts#L6 // We could import it from the CLI to prevent code duplication, // but CLI is way too heavy to be used within an MCP server. const OSBasedPaths = envPaths('netlify', { suffix: '' }); const configPath = path.join(OSBasedPaths.config, 'config.json'); const configData = await fs.readFile(configPath, { encoding: 'utf-8' }); const parsedData = JSON.parse(configData.toString()); const userId = parsedData?.userId; return parsedData?.users?.[userId]?.auth?.token; } catch {} return ''; } export const userIsAuthenticated = async (request?: Request): Promise<boolean> => { try { const token = await getNetlifyAccessToken(request); if (!token) { return false; } const response = await authenticatedFetch('/api/v1/user', {}, request); if(response.status === 401) { return false; } } catch (error) { if (error instanceof NetlifyUnauthError) { return false; } throw error; // rethrow other errors } return true; } export const getNetlifyAccessToken = async (request?: Request): Promise<string> => { if (request) { const authHeader = request.headers.get('Authorization'); let token = ''; if (authHeader && authHeader.startsWith('Bearer ')) { const bearerToken = authHeader.slice(7); if(bearerToken.startsWith('nfu') || bearerToken.startsWith('nfp') || bearerToken.startsWith('nfo')){ token = bearerToken; }else { const decrypted = await decryptJWE(bearerToken) ; if(decrypted && typeof decrypted.accessToken === 'string') { token = decrypted.accessToken; } else { console.error(`decrypted JWE did not contain accessToken. fields it does have:`, Object.keys(decrypted)); } } } if(!token) { throw new NetlifyUnauthError('no Bearer token found in Authorization header'); } return token; } let token = ''; // allow the PAT to be set just in case if (process.env.NETLIFY_PERSONAL_ACCESS_TOKEN) { return process.env.NETLIFY_PERSONAL_ACCESS_TOKEN; } token = await readTokenFromEnv(); if (!token) { const result = await runCommand('netlify login', { env: process.env }); appendToLog(["Netlify login exit code and output", JSON.stringify(result)]); if (result.exitCode === 0) { token = await readTokenFromEnv(); } if (!token) { throw new NetlifyUnauthError(getAuthTokenMsg); } } return token; } export const unauthenticatedFetch = async (url: string, options: RequestInit = {}) => { const response = await fetch(url, { ...options, headers: { 'user-agent': 'netlify-mcp', ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(options.headers || {}) }, }); return response; } export const authenticatedFetch = async (urlOrPath: string, options: RequestInit = {}, incomingRequest?: Request) => { const token = await getNetlifyAccessToken(incomingRequest); const url = new URL(urlOrPath, 'https://api.netlify.com') return unauthenticatedFetch(url.toString(), { ...options, headers: { 'Authorization': `Bearer ${token}`, ...(options.headers || {}) }, }); } export const getAPIJSONResult = async (urlOrPath: string, options: RequestInit = {}, apiInteractionOptions: APIInteractionOptions = {}, incomingRequest?: Request): Promise<any> => { if(!apiInteractionOptions.pagination){ const response = await authenticatedFetch(urlOrPath, options, incomingRequest); if(response.status === 401 && incomingRequest) { throw new NetlifyUnauthError(`Unauthedenticated request to Netlify API. ${urlOrPath}`); } if (!response.ok) { if(apiInteractionOptions.failureCallback){ return apiInteractionOptions.failureCallback(response); } throw new Error(`Failed to fetch API: ${response.status}`); } const data = await response.text(); if (!data) { return ''; } try{ return JSON.parse(data); } catch (e) { if (apiInteractionOptions.failureCallback) { return apiInteractionOptions.failureCallback(response); } return data; } } const currentTime = Date.now(); const maxDuration = 22000; // 22 seconds let apiResults = []; let page = 1 + (apiInteractionOptions.pageOffset || 0); // avoid unbounded requests let pageLimit = apiInteractionOptions.pageLimit || 100; const pageSize = apiInteractionOptions.pageSize || 20; while (true) { const url = new URL(urlOrPath, 'https://api.netlify.com') url.searchParams.set('page', page.toString()); url.searchParams.set('page_size', pageSize.toString()); const response = await authenticatedFetch(url.toString(), options, incomingRequest); if (!response.ok) { if (apiInteractionOptions.failureCallback) { return apiInteractionOptions.failureCallback(response); } throw new Error(`Failed to fetch API: ${response.status}`); } const resultRaw = await response.text(); if (!resultRaw) { break; } const result = JSON.parse(resultRaw); const lastResultTime = Date.now(); const duration = (lastResultTime - currentTime) / 1000; appendToLog(`Fetched page ${page}, received ${result.length} sites, total ${apiResults.length}, duration: ${duration} seconds`); if (Array.isArray(result)) { apiResults.push(...result); appendToLog(`Fetched page ${page}, received ${result.length} sites, total ${apiResults.length}`); page++; if (result.length < pageSize || page > pageLimit || duration > maxDuration) { break; } } else { break; } } return apiResults; } export type NetlifySite = { id: string; name: string; url: string; ssl_url: string; admin_url: string; user_id: string; account_id: string; account_slug: string; account_name: string; account_type: string; }; export const getSiteId = async ({ projectDir }: { projectDir: string }): Promise<string> => { const netlifySiteStatePath = path.join(projectDir, '.netlify', 'state.json'); const data = await fs.readFile(netlifySiteStatePath); const parsedData = JSON.parse(data.toString()); return parsedData.siteId; } export const getSite = async ({ siteId, incomingRequest }: { siteId: string, incomingRequest?: Request }): Promise<NetlifySite> => { const res = await authenticatedFetch(`/api/v1/sites/${siteId}`, {}, incomingRequest); if (!res.ok) { const data = await res.json(); throw new Error(`Failed to fetch sites, status: ${res.status}, ${data.message}`); } return await res.json(); }

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