Skip to main content
Glama
netlify

Netlify MCP Server

Official
by netlify
extension-utils.ts6.93 kB
import path from 'node:path'; import * as fs from 'node:fs/promises'; import { appendToLog } from '../../utils/logging.js'; import { type JsonSchema7Type } from 'zod-to-json-schema'; import { authenticatedFetch, getNetlifyAccessToken, unauthenticatedFetch } from '../../utils/api-networking.js'; const sdkBaseUrl = 'https://api.netlifysdk.com'; const netlifyFunctionsBaseUrl = 'https://app.netlify.com/.netlify/functions'; 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 type NetlifyExtension = { id: number; name: string; visibility: string; slug: string; identeerSlug: string; scopes: string; description: string; hostSiteId: string; hostSiteUrl: string; integrationLevel: string; category: string; author: string; lightLogoPath: string; darkLogoPath: string; v1Migrated: boolean; sdkVersion?: string; installedOnTeam?: boolean; hasIntegrationUI?: boolean; uiSurfaces?: string[]; hasDataIntegration?: boolean; hasBuildEventHandler?: boolean; isOwnedByTeam?: boolean; isPartner?: boolean; details?: string; numInstallations?: number; // added internally by MCP siteLevelConfigurationUrl?: string; trpcEndpoint?: string; trpcProcedures?: TRPCProcedure[]; }; export type TRPCProcedure = { name: string; type: 'query' | 'mutation' | 'subscription'; inputSchema: (JsonSchema7Type & { $schema: string }) | null; }; 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(); } export const getExtensions = async (): Promise<NetlifyExtension[]> => { const res = await unauthenticatedFetch(`${sdkBaseUrl}/integrations`); if (!res.ok) { throw new Error(`Failed to fetch extensions`); } return await res.json(); } export const getExtension = async ({ accountId, extensionSlug, request }: { accountId: string; extensionSlug: string; request?: Request }): Promise<NetlifyExtension> => { const res = await authenticatedFetch( `${sdkBaseUrl}/${encodeURIComponent(accountId)}/integrations/${encodeURIComponent(extensionSlug)}`, { headers: { 'netlify-token': await getNetlifyAccessToken(request), 'Api-Version': '2' } }, request ); if (!res.ok) { throw new Error(`Failed to fetch extension with slug '${extensionSlug}'`); } const extensionData: NetlifyExtension = await res.json(); return extensionData; } export const changeExtensionInstallation = async ({ shouldBeInstalled, accountId, extensionSlug, request }: { shouldBeInstalled: boolean; accountId: string; extensionSlug: string; request?: Request; }): Promise<NetlifyExtension> => { const extensionData = await getExtension({ accountId, extensionSlug, request }); const res = await authenticatedFetch(`${netlifyFunctionsBaseUrl}/${shouldBeInstalled ? 'install' : 'uninstall'}-extension`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: `_nf-auth=${await getNetlifyAccessToken(request)}` }, body: JSON.stringify({ teamId: accountId, slug: extensionSlug, hostSiteUrl: extensionData.hostSiteUrl }) }, request); if (!res.ok) { throw new Error(`Failed to install extension: ${extensionSlug}`); } const installExtensionData = await res.json(); return installExtensionData; } const getSDKToken = async ({ accountId, extensionSlug, request }: { accountId: string; extensionSlug: string; request?: Request; }): Promise<string> => { const res = await authenticatedFetch(`${sdkBaseUrl}/generate-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Api-Version': '2', Cookie: `_nf-auth=${await getNetlifyAccessToken(request)}` }, body: JSON.stringify({ ownerId: accountId, integrationSlug: extensionSlug }) }, request); if (!res.ok) { const data = await res.json(); throw new Error(`Failed to get SDK token, status: ${res.status}, ${data.message}`); } const { token } = await res.json(); return token; } const callExtensionAPI = async ({ site, extensionData, sdkToken, trpcProcedureName, trpcInput }: { site: NetlifySite; extensionData: NetlifyExtension; sdkToken: string; trpcProcedureName: string; trpcInput?: any; }): Promise<void> => { if (!extensionData.trpcEndpoint || extensionData.trpcProcedures?.length === 0) { throw new Error(`Extension "${extensionData.name}" does not have trpcEndpoint or trpcProcedures.`); } const trpcProcedure = extensionData.trpcProcedures?.find( (trpcProcedure) => trpcProcedure.name === trpcProcedureName ); if (!trpcProcedure) { throw new Error( `Extension "${extensionData.name}" does not have TRPC procedure named "${trpcProcedureName}".` ); } if (trpcProcedure.type === 'subscription') { throw new Error('TRPC procedures of type "subscription" not supported.'); } const endpoint = `${extensionData.trpcEndpoint}/${trpcProcedureName}`; const method = trpcProcedure.type === 'query' ? 'GET' : 'POST'; // TODO: validate TRPC input and throw an error so AI could fix it const body = JSON.stringify(trpcInput); appendToLog([ `configure extension for extensionId: ${extensionData.id}, ` + `extensionSlug: ${extensionData.slug}, siteId: ${site.id}, ` + `accountId: ${site.account_id}, userId: ${site.user_id}, ` + `trpcProcedureName: ${trpcProcedureName}, trpcInput: ${body}` ]); const res = await fetch(`${extensionData.hostSiteUrl}${endpoint}`, { method, headers: { 'Content-Type': 'application/json', 'nf-uiext-extension-id': String(extensionData.id), 'nf-uiext-extension-slug': extensionData.slug, 'nf-uiext-netlify-token': sdkToken, 'nf-uiext-site-id': site.id, 'nf-uiext-team-id': site.account_id, 'nf-uiext-user-id': site.user_id }, body: body }); const data = await res.json(); if (!res.ok) { throw new Error(`Failed to configure extension, status: ${res.status}, ${data.message}`); } appendToLog([`successfully invoked extension API, response:`, data]); return data; }

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